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. –>
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());
+ }
+}