diff --git a/pom.xml b/pom.xml index 57ce574ce..38c4d5059 100644 --- a/pom.xml +++ b/pom.xml @@ -375,57 +375,14 @@ - - com.google.code.maven-replacer-plugin - replacer - 1.5.3 - - - add-request-interface - generate-resources - - replace - - - ${basedir}/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/*Request.json - false - ${project.build.directory}/generated-resources - - - "definitions" - "javaInterfaces" : ["de.rwth.idsg.ocpp.jaxb.RequestType"], - "definitions" - - - - - - add-response-interface - generate-resources - - replace - - - ${basedir}/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/*Response.json - false - ${project.build.directory}/generated-resources - - - "definitions" - "javaInterfaces" : ["de.rwth.idsg.ocpp.jaxb.ResponseType"], - "definitions" - - - - - - org.jsonschema2pojo jsonschema2pojo-maven-plugin 1.2.1 - ${project.build.directory}/generated-resources + + ${basedir}/src/main/resources/OCPP-2.0.1_part3_JSON_schemas + ${project.build.directory}/generated-sources ocpp._2020._03 true @@ -437,7 +394,7 @@ - process-resources + generate-sources generate @@ -472,24 +429,24 @@ MYSQL mysql:8.0 - root + root <!– Use root because of permissions –> ${db-password} ${db-schema} - + <!– https://github.com/steve-community/steve/issues/157 –> SET default_storage_engine=InnoDB; - + be executed in the extended version as well –> true - + fix this. This fallback mechanism will be removed in Flyway 6.0.0. –> schema_version
true @@ -504,7 +461,7 @@ filesystem:src/main/resources/db/migration
- + <!– Generator parameters –> @@ -517,7 +474,7 @@ BOOLEAN - + <!– https://github.com/jOOQ/jOOQ/issues/7719 –> (?i:(TINY|SMALL|MEDIUM|BIG)?INT(UNSIGNED)?\(1\)) diff --git a/src/main/java/de/rwth/idsg/steve/service/ChargeboxService.java b/src/main/java/de/rwth/idsg/steve/service/ChargeboxService.java new file mode 100644 index 000000000..4f981607b --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/service/ChargeboxService.java @@ -0,0 +1,55 @@ +/* + * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve + * Copyright (C) 2013-2025 SteVe Community Team + * All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package de.rwth.idsg.steve.service; + +import de.rwth.idsg.steve.repository.ChargePointRepository; +import de.rwth.idsg.steve.repository.dto.ChargePoint; +import de.rwth.idsg.steve.web.dto.ChargePointForm; +import de.rwth.idsg.steve.web.dto.ChargePointQueryForm; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChargeboxService { + + private final ChargePointRepository chargePointRepository; + + public List getOverview(ChargePointQueryForm form) { + return chargePointRepository.getOverview(form); + } + + public ChargePoint.Details getDetails(int chargeBoxPk) { + return chargePointRepository.getDetails(chargeBoxPk); + } + + public int addChargePoint(ChargePointForm form) { + return chargePointRepository.addChargePoint(form); + } + + public void updateChargePoint(ChargePointForm form) { + chargePointRepository.updateChargePoint(form); + } + + public void deleteChargePoint(int chargeBoxPk) { + chargePointRepository.deleteChargePoint(chargeBoxPk); + } +} diff --git a/src/main/java/de/rwth/idsg/steve/service/ReservationService.java b/src/main/java/de/rwth/idsg/steve/service/ReservationService.java new file mode 100644 index 000000000..48721c058 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/service/ReservationService.java @@ -0,0 +1,37 @@ +package de.rwth.idsg.steve.service; + +import de.rwth.idsg.steve.repository.ReservationRepository; +import de.rwth.idsg.steve.repository.dto.InsertReservationParams; +import de.rwth.idsg.steve.repository.dto.Reservation; +import de.rwth.idsg.steve.web.dto.ReservationForm; +import de.rwth.idsg.steve.web.dto.ReservationQueryForm; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationRepository reservationRepository; + + public List getReservations(ReservationQueryForm form) { + return reservationRepository.getReservations(form); + } + + public int addReservation(ReservationForm form) { + InsertReservationParams params = InsertReservationParams.builder() + .idTag(form.getIdTag()) + .chargeBoxId(form.getChargeBoxId()) + .connectorId(form.getConnectorId()) + .startTimestamp(form.getStartTimestamp()) + .expiryTimestamp(form.getExpiryTimestamp()) + .build(); + return reservationRepository.insert(params); + } + + public void deleteReservation(int reservationId) { + reservationRepository.delete(reservationId); + } +} diff --git a/src/main/java/de/rwth/idsg/steve/service/UserService.java b/src/main/java/de/rwth/idsg/steve/service/UserService.java new file mode 100644 index 000000000..4ac3d3837 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/service/UserService.java @@ -0,0 +1,37 @@ +package de.rwth.idsg.steve.service; + +import de.rwth.idsg.steve.repository.UserRepository; +import de.rwth.idsg.steve.repository.dto.User; +import de.rwth.idsg.steve.web.dto.UserForm; +import de.rwth.idsg.steve.web.dto.UserQueryForm; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public List getUsers(UserQueryForm form) { + return userRepository.getOverview(form); + } + + public User.Details getUser(int userPk) { + return userRepository.getDetails(userPk); + } + + public void addUser(UserForm form) { + userRepository.add(form); + } + + public void updateUser(UserForm form) { + userRepository.update(form); + } + + public void deleteUser(int userPk) { + userRepository.delete(userPk); + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/api/ChargeboxRestController.java b/src/main/java/de/rwth/idsg/steve/web/api/ChargeboxRestController.java new file mode 100644 index 000000000..d18bffaae --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/api/ChargeboxRestController.java @@ -0,0 +1,148 @@ +/* + * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve + * Copyright (C) 2013-2025 SteVe Community Team + * All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.repository.dto.ChargePoint; +import de.rwth.idsg.steve.service.ChargeboxService; +import de.rwth.idsg.steve.web.api.ApiControllerAdvice.ApiErrorResponse; +import de.rwth.idsg.steve.web.dto.ChargePointForm; +import de.rwth.idsg.steve.web.dto.ChargePointQueryForm; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Tag(name = "chargebox-controller", description = "Operations related to managing Chargeboxes.") +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/chargeboxes", produces = MediaType.APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +public class ChargeboxRestController { + + private final ChargeboxService chargeboxService; + + @Operation(description = "Returns a list of Chargeboxes based on the query parameters.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))})} + ) + @GetMapping(value = "") + @ResponseBody + public List get(@ParameterObject ChargePointQueryForm params) { + log.debug("Read request for query: {}", params); + List response = chargeboxService.getOverview(params); + log.debug("Read response for query: {}", response); + return response; + } + + @Operation(description = "Returns a single Chargebox based on the chargeBoxPk.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "Not Found", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))})} + ) + @GetMapping("/{chargeBoxPk}") + @ResponseBody + public ChargePoint.Details getOne(@PathVariable("chargeBoxPk") Integer chargeBoxPk) { + log.debug("Read request for chargeBoxPk: {}", chargeBoxPk); + ChargePoint.Details response = chargeboxService.getDetails(chargeBoxPk); + log.debug("Read response: {}", response); + return response; + } + + @Operation(description = "Creates a new Chargebox with the provided parameters.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Created"), + @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "422", description = "Unprocessable Entity", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))})} + ) + @PostMapping + @ResponseBody + @ResponseStatus(HttpStatus.CREATED) + public ChargePoint.Details create(@RequestBody @Valid ChargePointForm params) { + log.debug("Create request: {}", params); + int chargeBoxPk = chargeboxService.addChargePoint(params); + ChargePoint.Details response = chargeboxService.getDetails(chargeBoxPk); + log.debug("Create response: {}", response); + return response; + } + + @Operation(description = "Updates an existing Chargebox with the provided parameters.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "Not Found", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))})} + ) + @PutMapping("/{chargeBoxPk}") + @ResponseBody + public ChargePoint.Details update(@PathVariable("chargeBoxPk") Integer chargeBoxPk, @RequestBody @Valid ChargePointForm params) { + params.setChargeBoxPk(chargeBoxPk); + log.debug("Update request: {}", params); + chargeboxService.updateChargePoint(params); + ChargePoint.Details response = chargeboxService.getDetails(chargeBoxPk); + log.debug("Update response: {}", response); + return response; + } + + @Operation(description = "Deletes an existing Chargebox based on the chargeBoxPk.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "404", description = "Not Found", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))}), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))})} + ) + @DeleteMapping("/{chargeBoxPk}") + @ResponseBody + public ChargePoint.Details delete(@PathVariable("chargeBoxPk") Integer chargeBoxPk) { + log.debug("Delete request for chargeBoxPk: {}", chargeBoxPk); + ChargePoint.Details response = chargeboxService.getDetails(chargeBoxPk); + chargeboxService.deleteChargePoint(chargeBoxPk); + log.debug("Delete response: {}", response); + return response; + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/api/ReservationRestController.java b/src/main/java/de/rwth/idsg/steve/web/api/ReservationRestController.java new file mode 100644 index 000000000..126328c3d --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/api/ReservationRestController.java @@ -0,0 +1,38 @@ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.repository.dto.Reservation; +import de.rwth.idsg.steve.service.ReservationService; +import de.rwth.idsg.steve.web.dto.ReservationForm; +import de.rwth.idsg.steve.web.dto.ReservationQueryForm; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping(path = "/api/reservations", produces = "application/json;charset=UTF-8") +@RequiredArgsConstructor +public class ReservationRestController { + + private final ReservationService reservationService; + + @GetMapping + public ResponseEntity> getReservations(ReservationQueryForm form) { + List reservations = reservationService.getReservations(form); + return ResponseEntity.ok(reservations); + } + + @PostMapping + public ResponseEntity addReservation(@Valid @RequestBody ReservationForm form) { + int reservationId = reservationService.addReservation(form); + return ResponseEntity.ok(reservationId); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteReservation(@PathVariable int id) { + reservationService.deleteReservation(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/api/UserRestController.java b/src/main/java/de/rwth/idsg/steve/web/api/UserRestController.java new file mode 100644 index 000000000..5e31f0b9e --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/api/UserRestController.java @@ -0,0 +1,51 @@ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.repository.dto.User; +import de.rwth.idsg.steve.service.UserService; +import de.rwth.idsg.steve.web.dto.UserForm; +import de.rwth.idsg.steve.web.dto.UserQueryForm; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping(path = "/api/users", produces = "application/json;charset=UTF-8") +@RequiredArgsConstructor +public class UserRestController { + + private final UserService userService; + + @GetMapping + public ResponseEntity> getUsers(UserQueryForm form) { + List users = userService.getUsers(form); + return ResponseEntity.ok(users); + } + + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable int id) { + User.Details user = userService.getUser(id); + return ResponseEntity.ok(user); + } + + @PostMapping + public ResponseEntity addUser(@Valid @RequestBody UserForm form) { + userService.addUser(form); + return ResponseEntity.ok().build(); + } + + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable int id, @Valid @RequestBody UserForm form) { + form.setUserPk(id); + userService.updateUser(form); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable int id) { + userService.deleteUser(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/ReservationForm.java b/src/main/java/de/rwth/idsg/steve/web/dto/ReservationForm.java new file mode 100644 index 000000000..09a3cb224 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/dto/ReservationForm.java @@ -0,0 +1,33 @@ +package de.rwth.idsg.steve.web.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +public class ReservationForm { + + private Integer reservationPk; + + @NotEmpty(message = "ID Tag is required") + private String idTag; + + @NotEmpty(message = "ChargeBox ID is required") + private String chargeBoxId; + + @NotNull(message = "Connector ID is required") + private Integer connectorId; + + @NotNull(message = "Start timestamp is required") + private LocalDateTime startTimestamp; + + @NotNull(message = "Expiry timestamp is required") + @Future(message = "Expiry timestamp must be in the future") + private LocalDateTime expiryTimestamp; +} diff --git a/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeRequest.json b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeRequest.json index 5d88d2df4..7f31b45fe 100644 --- a/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeRequest.json +++ b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeRequest.json @@ -4,141 +4,13 @@ "comment": "OCPP 2.0.1 FINAL", "definitions": { "CustomDataType": { - "description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.", - "javaType": "CustomData", - "type": "object", - "properties": { - "vendorId": { - "type": "string", - "maxLength": 255 - } - }, - "required": [ - "vendorId" - ] - }, - "HashAlgorithmEnumType": { - "description": "Used algorithms for the hashes provided.\r\n", - "javaType": "HashAlgorithmEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "SHA256", - "SHA384", - "SHA512" - ] - }, - "IdTokenEnumType": { - "description": "Enumeration of possible idToken types.\r\n", - "javaType": "IdTokenEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "Central", - "eMAID", - "ISO14443", - "ISO15693", - "KeyCode", - "Local", - "MacAddress", - "NoAuthorization" - ] - }, - "AdditionalInfoType": { - "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", - "javaType": "AdditionalInfo", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "additionalIdToken": { - "description": "This field specifies the additional IdToken.\r\n", - "type": "string", - "maxLength": 36 - }, - "type": { - "description": "This defines the type of the additionalIdToken. This is a custom type, so the implementation needs to be agreed upon by all involved parties.\r\n", - "type": "string", - "maxLength": 50 - } - }, - "required": [ - "additionalIdToken", - "type" - ] + "$ref": "common/Common.json#/definitions/CustomDataType" }, "IdTokenType": { - "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", - "javaType": "IdToken", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "additionalInfo": { - "type": "array", - "additionalItems": false, - "items": { - "$ref": "#/definitions/AdditionalInfoType" - }, - "minItems": 1 - }, - "idToken": { - "description": "IdToken is case insensitive. Might hold the hidden id of an RFID tag, but can for example also contain a UUID.\r\n", - "type": "string", - "maxLength": 36 - }, - "type": { - "$ref": "#/definitions/IdTokenEnumType" - } - }, - "required": [ - "idToken", - "type" - ] + "$ref": "common/Common.json#/definitions/IdTokenType" }, "OCSPRequestDataType": { - "javaType": "OCSPRequestData", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "hashAlgorithm": { - "$ref": "#/definitions/HashAlgorithmEnumType" - }, - "issuerNameHash": { - "description": "Hashed value of the Issuer DN (Distinguished Name).\r\n\r\n", - "type": "string", - "maxLength": 128 - }, - "issuerKeyHash": { - "description": "Hashed value of the issuers public key\r\n", - "type": "string", - "maxLength": 128 - }, - "serialNumber": { - "description": "The serial number of the certificate.\r\n", - "type": "string", - "maxLength": 40 - }, - "responderURL": { - "description": "This contains the responder URL (Case insensitive). \r\n\r\n", - "type": "string", - "maxLength": 512 - } - }, - "required": [ - "hashAlgorithm", - "issuerNameHash", - "issuerKeyHash", - "serialNumber", - "responderURL" - ] + "$ref": "common/Common.json#/definitions/OCSPRequestDataType" } }, "type": "object", diff --git a/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeResponse.json b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeResponse.json index 3de6f3337..b9e2600f0 100644 --- a/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeResponse.json +++ b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/AuthorizeResponse.json @@ -4,214 +4,13 @@ "comment": "OCPP 2.0.1 FINAL", "definitions": { "CustomDataType": { - "description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.", - "javaType": "CustomData", - "type": "object", - "properties": { - "vendorId": { - "type": "string", - "maxLength": 255 - } - }, - "required": [ - "vendorId" - ] - }, - "AuthorizationStatusEnumType": { - "description": "ID_ Token. Status. Authorization_ Status\r\nurn:x-oca:ocpp:uid:1:569372\r\nCurrent status of the ID Token.\r\n", - "javaType": "AuthorizationStatusEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "Accepted", - "Blocked", - "ConcurrentTx", - "Expired", - "Invalid", - "NoCredit", - "NotAllowedTypeEVSE", - "NotAtThisLocation", - "NotAtThisTime", - "Unknown" - ] - }, - "AuthorizeCertificateStatusEnumType": { - "description": "Certificate status information. \r\n- if all certificates are valid: return 'Accepted'.\r\n- if one of the certificates was revoked, return 'CertificateRevoked'.\r\n", - "javaType": "AuthorizeCertificateStatusEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "Accepted", - "SignatureError", - "CertificateExpired", - "CertificateRevoked", - "NoCertificateAvailable", - "CertChainError", - "ContractCancelled" - ] - }, - "IdTokenEnumType": { - "description": "Enumeration of possible idToken types.\r\n", - "javaType": "IdTokenEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "Central", - "eMAID", - "ISO14443", - "ISO15693", - "KeyCode", - "Local", - "MacAddress", - "NoAuthorization" - ] - }, - "MessageFormatEnumType": { - "description": "Message_ Content. Format. Message_ Format_ Code\r\nurn:x-enexis:ecdm:uid:1:570848\r\nFormat of the message.\r\n", - "javaType": "MessageFormatEnum", - "type": "string", - "additionalProperties": false, - "enum": [ - "ASCII", - "HTML", - "URI", - "UTF8" - ] - }, - "AdditionalInfoType": { - "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", - "javaType": "AdditionalInfo", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "additionalIdToken": { - "description": "This field specifies the additional IdToken.\r\n", - "type": "string", - "maxLength": 36 - }, - "type": { - "description": "This defines the type of the additionalIdToken. This is a custom type, so the implementation needs to be agreed upon by all involved parties.\r\n", - "type": "string", - "maxLength": 50 - } - }, - "required": [ - "additionalIdToken", - "type" - ] + "$ref": "common/Common.json#/definitions/CustomDataType" }, "IdTokenInfoType": { - "description": "ID_ Token\r\nurn:x-oca:ocpp:uid:2:233247\r\nContains status information about an identifier.\r\nIt is advised to not stop charging for a token that expires during charging, as ExpiryDate is only used for caching purposes. If ExpiryDate is not given, the status has no end date.\r\n", - "javaType": "IdTokenInfo", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "status": { - "$ref": "#/definitions/AuthorizationStatusEnumType" - }, - "cacheExpiryDateTime": { - "description": "ID_ Token. Expiry. Date_ Time\r\nurn:x-oca:ocpp:uid:1:569373\r\nDate and Time after which the token must be considered invalid.\r\n", - "type": "string", - "format": "date-time" - }, - "chargingPriority": { - "description": "Priority from a business point of view. Default priority is 0, The range is from -9 to 9. Higher values indicate a higher priority. The chargingPriority in <<transactioneventresponse,TransactionEventResponse>> overrules this one. \r\n", - "type": "integer" - }, - "language1": { - "description": "ID_ Token. Language1. Language_ Code\r\nurn:x-oca:ocpp:uid:1:569374\r\nPreferred user interface language of identifier user. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n\r\n", - "type": "string", - "maxLength": 8 - }, - "evseId": { - "description": "Only used when the IdToken is only valid for one or more specific EVSEs, not for the entire Charging Station.\r\n\r\n", - "type": "array", - "additionalItems": false, - "items": { - "type": "integer" - }, - "minItems": 1 - }, - "groupIdToken": { - "$ref": "#/definitions/IdTokenType" - }, - "language2": { - "description": "ID_ Token. Language2. Language_ Code\r\nurn:x-oca:ocpp:uid:1:569375\r\nSecond preferred user interface language of identifier user. Don’t use when language1 is omitted, has to be different from language1. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n", - "type": "string", - "maxLength": 8 - }, - "personalMessage": { - "$ref": "#/definitions/MessageContentType" - } - }, - "required": [ - "status" - ] + "$ref": "common/Common.json#/definitions/IdTokenInfoType" }, - "IdTokenType": { - "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", - "javaType": "IdToken", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "additionalInfo": { - "type": "array", - "additionalItems": false, - "items": { - "$ref": "#/definitions/AdditionalInfoType" - }, - "minItems": 1 - }, - "idToken": { - "description": "IdToken is case insensitive. Might hold the hidden id of an RFID tag, but can for example also contain a UUID.\r\n", - "type": "string", - "maxLength": 36 - }, - "type": { - "$ref": "#/definitions/IdTokenEnumType" - } - }, - "required": [ - "idToken", - "type" - ] - }, - "MessageContentType": { - "description": "Message_ Content\r\nurn:x-enexis:ecdm:uid:2:234490\r\nContains message details, for a message to be displayed on a Charging Station.\r\n\r\n", - "javaType": "MessageContent", - "type": "object", - "additionalProperties": false, - "properties": { - "customData": { - "$ref": "#/definitions/CustomDataType" - }, - "format": { - "$ref": "#/definitions/MessageFormatEnumType" - }, - "language": { - "description": "Message_ Content. Language. Language_ Code\r\nurn:x-enexis:ecdm:uid:1:570849\r\nMessage language identifier. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n", - "type": "string", - "maxLength": 8 - }, - "content": { - "description": "Message_ Content. Content. Message\r\nurn:x-enexis:ecdm:uid:1:570852\r\nMessage contents.\r\n\r\n", - "type": "string", - "maxLength": 512 - } - }, - "required": [ - "format", - "content" - ] + "AuthorizeCertificateStatusEnumType": { + "$ref": "common/Common.json#/definitions/AuthorizeCertificateStatusEnumType" } }, "type": "object", diff --git a/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/common/Common.json b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/common/Common.json new file mode 100644 index 000000000..b19fadfb7 --- /dev/null +++ b/src/main/resources/OCPP-2.0.1_part3_JSON_schemas/common/Common.json @@ -0,0 +1,268 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "urn:OCPP:Cp:2:2020:3:Common", + "comment": "OCPP 2.0.1 FINAL", + "definitions": { + "CustomDataType": { + "description": "This class does not get 'AdditionalProperties = false' in the schema generation, so it can be extended with arbitrary JSON properties to allow adding custom data.", + "javaType": "CustomData", + "type": "object", + "properties": { + "vendorId": { + "type": "string", + "maxLength": 255 + } + }, + "required": [ + "vendorId" + ] + }, + "HashAlgorithmEnumType": { + "description": "Used algorithms for the hashes provided.\r\n", + "javaType": "HashAlgorithmEnum", + "type": "string", + "additionalProperties": false, + "enum": [ + "SHA256", + "SHA384", + "SHA512" + ] + }, + "IdTokenEnumType": { + "description": "Enumeration of possible idToken types.\r\n", + "javaType": "IdTokenEnum", + "type": "string", + "additionalProperties": false, + "enum": [ + "Central", + "eMAID", + "ISO14443", + "ISO15693", + "KeyCode", + "Local", + "MacAddress", + "NoAuthorization" + ] + }, + "AdditionalInfoType": { + "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", + "javaType": "AdditionalInfo", + "type": "object", + "additionalProperties": false, + "properties": { + "customData": { + "$ref": "#/definitions/CustomDataType" + }, + "additionalIdToken": { + "description": "This field specifies the additional IdToken.\r\n", + "type": "string", + "maxLength": 36 + }, + "type": { + "description": "This defines the type of the additionalIdToken. This is a custom type, so the implementation needs to be agreed upon by all involved parties.\r\n", + "type": "string", + "maxLength": 50 + } + }, + "required": [ + "additionalIdToken", + "type" + ] + }, + "IdTokenType": { + "description": "Contains a case insensitive identifier to use for the authorization and the type of authorization to support multiple forms of identifiers.\r\n", + "javaType": "IdToken", + "type": "object", + "additionalProperties": false, + "properties": { + "customData": { + "$ref": "#/definitions/CustomDataType" + }, + "additionalInfo": { + "type": "array", + "additionalItems": false, + "items": { + "$ref": "#/definitions/AdditionalInfoType" + }, + "minItems": 1 + }, + "idToken": { + "description": "IdToken is case insensitive. Might hold the hidden id of an RFID tag, but can for example also contain a UUID.\r\n", + "type": "string", + "maxLength": 36 + }, + "type": { + "$ref": "#/definitions/IdTokenEnumType" + } + }, + "required": [ + "idToken", + "type" + ] + }, + "OCSPRequestDataType": { + "javaType": "OCSPRequestData", + "type": "object", + "additionalProperties": false, + "properties": { + "customData": { + "$ref": "#/definitions/CustomDataType" + }, + "hashAlgorithm": { + "$ref": "#/definitions/HashAlgorithmEnumType" + }, + "issuerNameHash": { + "description": "Hashed value of the Issuer DN (Distinguished Name).\r\n\r\n", + "type": "string", + "maxLength": 128 + }, + "issuerKeyHash": { + "description": "Hashed value of the issuers public key\r\n", + "type": "string", + "maxLength": 128 + }, + "serialNumber": { + "description": "The serial number of the certificate.\r\n", + "type": "string", + "maxLength": 40 + }, + "responderURL": { + "description": "This contains the responder URL (Case insensitive). \r\n\r\n", + "type": "string", + "maxLength": 512 + } + }, + "required": [ + "hashAlgorithm", + "issuerNameHash", + "issuerKeyHash", + "serialNumber", + "responderURL" + ] + }, + "AuthorizationStatusEnumType": { + "description": "ID_ Token. Status. Authorization_ Status\r\nurn:x-oca:ocpp:uid:1:569372\r\nCurrent status of the ID Token.\r\n", + "javaType": "AuthorizationStatusEnum", + "type": "string", + "additionalProperties": false, + "enum": [ + "Accepted", + "Blocked", + "ConcurrentTx", + "Expired", + "Invalid", + "NoCredit", + "NotAllowedTypeEVSE", + "NotAtThisLocation", + "NotAtThisTime", + "Unknown" + ] + }, + "AuthorizeCertificateStatusEnumType": { + "description": "Certificate status information. \r\n- if all certificates are valid: return 'Accepted'.\r\n- if one of the certificates was revoked, return 'CertificateRevoked'.\r\n", + "javaType": "AuthorizeCertificateStatusEnum", + "type": "string", + "additionalProperties": false, + "enum": [ + "Accepted", + "SignatureError", + "CertificateExpired", + "CertificateRevoked", + "NoCertificateAvailable", + "CertChainError", + "ContractCancelled" + ] + }, + "MessageFormatEnumType": { + "description": "Message_ Content. Format. Message_ Format_ Code\r\nurn:x-enexis:ecdm:uid:1:570848\r\nFormat of the message.\r\n", + "javaType": "MessageFormatEnum", + "type": "string", + "additionalProperties": false, + "enum": [ + "ASCII", + "HTML", + "URI", + "UTF8" + ] + }, + "MessageContentType": { + "description": "Message_ Content\r\nurn:x-enexis:ecdm:uid:2:234490\r\nContains message details, for a message to be displayed on a Charging Station.\r\n\r\n", + "javaType": "MessageContent", + "type": "object", + "additionalProperties": false, + "properties": { + "customData": { + "$ref": "#/definitions/CustomDataType" + }, + "format": { + "$ref": "#/definitions/MessageFormatEnumType" + }, + "language": { + "description": "Message_ Content. Language. Language_ Code\r\nurn:x-enexis:ecdm:uid:1:570849\r\nMessage language identifier. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n", + "type": "string", + "maxLength": 8 + }, + "content": { + "description": "Message_ Content. Content. Message\r\nurn:x-enexis:ecdm:uid:1:570852\r\nMessage contents.\r\n\r\n", + "type": "string", + "maxLength": 512 + } + }, + "required": [ + "format", + "content" + ] + }, + "IdTokenInfoType": { + "description": "ID_ Token\r\nurn:x-oca:ocpp:uid:2:233247\r\nContains status information about an identifier.\r\nIt is advised to not stop charging for a token that expires during charging, as ExpiryDate is only used for caching purposes. If ExpiryDate is not given, the status has no end date.\r\n", + "javaType": "IdTokenInfo", + "type": "object", + "additionalProperties": false, + "properties": { + "customData": { + "$ref": "#/definitions/CustomDataType" + }, + "status": { + "$ref": "#/definitions/AuthorizationStatusEnumType" + }, + "cacheExpiryDateTime": { + "description": "ID_ Token. Expiry. Date_ Time\r\nurn:x-oca:ocpp:uid:1:569373\r\nDate and Time after which the token must be considered invalid.\r\n", + "type": "string", + "format": "date-time" + }, + "chargingPriority": { + "description": "Priority from a business point of view. Default priority is 0, The range is from -9 to 9. Higher values indicate a higher priority. The chargingPriority in <<transactioneventresponse,TransactionEventResponse>> overrules this one. \r\n", + "type": "integer" + }, + "language1": { + "description": "ID_ Token. Language1. Language_ Code\r\nurn:x-oca:ocpp:uid:1:569374\r\nPreferred user interface language of identifier user. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n\r\n", + "type": "string", + "maxLength": 8 + }, + "evseId": { + "description": "Only used when the IdToken is only valid for one or more specific EVSEs, not for the entire Charging Station.\r\n\r\n", + "type": "array", + "additionalItems": false, + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "groupIdToken": { + "$ref": "#/definitions/IdTokenType" + }, + "language2": { + "description": "ID_ Token. Language2. Language_ Code\r\nurn:x-oca:ocpp:uid:1:569375\r\nSecond preferred user interface language of identifier user. Don’t use when language1 is omitted, has to be different from language1. Contains a language code as defined in <<ref-RFC5646,[RFC5646]>>.\r\n", + "type": "string", + "maxLength": 8 + }, + "personalMessage": { + "$ref": "#/definitions/MessageContentType" + } + }, + "required": [ + "status" + ] + } + } +} diff --git a/src/test/java/de/rwth/idsg/steve/web/api/ChargeboxRestControllerTest.java b/src/test/java/de/rwth/idsg/steve/web/api/ChargeboxRestControllerTest.java new file mode 100644 index 000000000..e6c0a3e8b --- /dev/null +++ b/src/test/java/de/rwth/idsg/steve/web/api/ChargeboxRestControllerTest.java @@ -0,0 +1,159 @@ +/* + * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve + * Copyright (C) 2013-2025 SteVe Community Team + * All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.repository.dto.ChargePoint; +import de.rwth.idsg.steve.service.ChargeboxService; +import de.rwth.idsg.steve.web.dto.ChargePointForm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class ChargeboxRestControllerTest extends AbstractControllerTest { + + private static final String CONTENT_TYPE = "application/json"; + + @Mock + private ChargeboxService chargeboxService; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders.standaloneSetup(new ChargeboxRestController(chargeboxService)) + .setControllerAdvice(new ApiControllerAdvice()) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) + .alwaysExpect(content().contentType(CONTENT_TYPE)) + .build(); + } + + @Test + @DisplayName("GET all: Test with empty results, expected 200") + public void testGet_withEmptyList() throws Exception { + when(chargeboxService.getOverview(any())).thenReturn(Collections.emptyList()); + + mockMvc.perform(get("/api/v1/chargeboxes")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @DisplayName("GET all: Test with one result, expected 200") + public void testGet_withOneResult() throws Exception { + List results = List.of(ChargePoint.Overview.builder().chargeBoxPk(1).build()); + when(chargeboxService.getOverview(any())).thenReturn(results); + + mockMvc.perform(get("/api/v1/chargeboxes")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].chargeBoxPk").value("1")); + } + + @Test + @DisplayName("GET one: Entity not found, expected 404") + public void testGetOne_notFound() throws Exception { + when(chargeboxService.getDetails(any(Integer.class))).thenReturn(null); + + mockMvc.perform(get("/api/v1/chargeboxes/1")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("GET one: One entity found, expected 200") + public void testGetOne_found() throws Exception { + ChargePoint.Details result = ChargePoint.Details.builder().chargeBoxPk(1).build(); + when(chargeboxService.getDetails(1)).thenReturn(result); + + mockMvc.perform(get("/api/v1/chargeboxes/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.chargeBoxPk").value("1")); + } + + @Test + @DisplayName("POST: Entity created, expected 201") + public void testPost() throws Exception { + ChargePointForm form = new ChargePointForm(); + form.setChargeBoxId("test-cb"); + + ChargePoint.Details result = ChargePoint.Details.builder().chargeBoxPk(1).chargeBoxId("test-cb").build(); + + when(chargeboxService.addChargePoint(any(ChargePointForm.class))).thenReturn(1); + when(chargeboxService.getDetails(1)).thenReturn(result); + + mockMvc.perform(post("/api/v1/chargeboxes") + .content(objectMapper.writeValueAsString(form)) + .contentType(CONTENT_TYPE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.chargeBoxPk").value("1")) + .andExpect(jsonPath("$.chargeBoxId").value("test-cb")); + } + + @Test + @DisplayName("PUT: Entity updated, expected 200") + public void testPut() throws Exception { + ChargePointForm form = new ChargePointForm(); + form.setChargeBoxId("test-cb-updated"); + + ChargePoint.Details result = ChargePoint.Details.builder().chargeBoxPk(1).chargeBoxId("test-cb-updated").build(); + + when(chargeboxService.getDetails(1)).thenReturn(result); + + mockMvc.perform(put("/api/v1/chargeboxes/1") + .content(objectMapper.writeValueAsString(form)) + .contentType(CONTENT_TYPE)) + .andExpect(status().isOk()); + + verify(chargeboxService).updateChargePoint(any(ChargePointForm.class)); + } + + @Test + @DisplayName("DELETE: Entity deleted, expected 200") + public void testDelete() throws Exception { + ChargePoint.Details result = ChargePoint.Details.builder().chargeBoxPk(1).build(); + when(chargeboxService.getDetails(1)).thenReturn(result); + + mockMvc.perform(delete("/api/v1/chargeboxes/1")) + .andExpect(status().isOk()); + + verify(chargeboxService).deleteChargePoint(eq(1)); + } +} diff --git a/src/test/java/de/rwth/idsg/steve/web/api/ReservationRestControllerTest.java b/src/test/java/de/rwth/idsg/steve/web/api/ReservationRestControllerTest.java new file mode 100644 index 000000000..4c092f666 --- /dev/null +++ b/src/test/java/de/rwth/idsg/steve/web/api/ReservationRestControllerTest.java @@ -0,0 +1,27 @@ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.service.ReservationService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ReservationRestController.class) +class ReservationRestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ReservationService reservationService; + + @Test + void testGetReservations() throws Exception { + mockMvc.perform(get("/api/reservations")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/de/rwth/idsg/steve/web/api/UserRestControllerTest.java b/src/test/java/de/rwth/idsg/steve/web/api/UserRestControllerTest.java new file mode 100644 index 000000000..2f4095632 --- /dev/null +++ b/src/test/java/de/rwth/idsg/steve/web/api/UserRestControllerTest.java @@ -0,0 +1,27 @@ +package de.rwth.idsg.steve.web.api; + +import de.rwth.idsg.steve.service.UserService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserRestController.class) +class UserRestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Test + void testGetUsers() throws Exception { + mockMvc.perform(get("/api/users")) + .andExpect(status().isOk()); + } +}