From 04bb9c82a3529f2cab5585b86614a64d7fd38fe0 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Tue, 9 Sep 2025 15:22:10 +0100 Subject: [PATCH 1/9] Updated gitignore to also ignore Intellij IDEA environment folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f91fa937..e545fd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.classpath /.settings /org.etsi.osl.tmf.api.iml +/.idea/ -- GitLab From d4ec52097e3a1ab6227da2c91613b3c6f3bebd0a Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Tue, 9 Sep 2025 15:25:45 +0100 Subject: [PATCH 2/9] Implemented range interval validation and type validation for Service Specification request body, plus 4 new tests --- .../configuration/RestExceptionHandler.java | 29 +- .../ServiceSpecificationApiController.java | 20 +- .../util/ServiceSpecificationValidator.java | 96 +++++ ...ServiceSpecificationApiControllerTest.java | 73 ++++ .../testServiceSpecInvalidRangeInterval.json | 43 ++ .../scm633/testServiceSpecInvalidTypes.json | 394 ++++++++++++++++++ .../testServiceSpecValidRangeInterval.json | 43 ++ .../scm633/testServiceSpecValidTypes.json | 394 ++++++++++++++++++ 8 files changed, 1074 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java create mode 100644 src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json create mode 100644 src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json create mode 100644 src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json create mode 100644 src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json diff --git a/src/main/java/org/etsi/osl/tmf/configuration/RestExceptionHandler.java b/src/main/java/org/etsi/osl/tmf/configuration/RestExceptionHandler.java index 42aac314..23442458 100644 --- a/src/main/java/org/etsi/osl/tmf/configuration/RestExceptionHandler.java +++ b/src/main/java/org/etsi/osl/tmf/configuration/RestExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -16,17 +17,25 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override - protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, - HttpHeaders headers, HttpStatusCode status, WebRequest request) { - String error = "Malformed JSON request"; - ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, error, ex); - return new ResponseEntity(apiError, apiError.getStatus()); - } + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, HttpStatusCode status, WebRequest request) { + return buildResponseEntity(ex); + } + // other exception handlers below - private ResponseEntity buildResponseEntity(ApiError apiError) { - return new ResponseEntity<>(apiError, apiError.getStatus()); + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + return buildResponseEntity(ex); } - // other exception handlers below -} \ No newline at end of file + private ResponseEntity buildResponseEntity(Throwable ex) { + String error = "Malformed JSON request"; + ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, error, ex); + return new ResponseEntity(apiError, apiError.getStatus()); + } +} diff --git a/src/main/java/org/etsi/osl/tmf/scm633/api/ServiceSpecificationApiController.java b/src/main/java/org/etsi/osl/tmf/scm633/api/ServiceSpecificationApiController.java index 12482fb6..d8944f1e 100644 --- a/src/main/java/org/etsi/osl/tmf/scm633/api/ServiceSpecificationApiController.java +++ b/src/main/java/org/etsi/osl/tmf/scm633/api/ServiceSpecificationApiController.java @@ -32,7 +32,6 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; -import org.etsi.osl.centrallog.client.CLevel; import org.etsi.osl.centrallog.client.CentralLogger; import org.etsi.osl.sd.model.ServiceDescriptor; import org.etsi.osl.tmf.common.model.Attachment; @@ -42,6 +41,7 @@ import org.etsi.osl.tmf.scm633.model.ServiceSpecificationCreate; import org.etsi.osl.tmf.scm633.model.ServiceSpecificationUpdate; import org.etsi.osl.tmf.scm633.reposervices.ServiceSpecificationRepoService; import org.etsi.osl.tmf.util.AddUserAsOwnerToRelatedParties; +import org.etsi.osl.tmf.util.ServiceSpecificationValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -52,15 +52,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -94,12 +89,21 @@ public class ServiceSpecificationApiController implements ServiceSpecificationAp @Autowired private CentralLogger centralLogger; + @Autowired + private ServiceSpecificationValidator serviceSpecificationValidator; + @org.springframework.beans.factory.annotation.Autowired public ServiceSpecificationApiController(ObjectMapper objectMapper, HttpServletRequest request) { this.objectMapper = objectMapper; this.request = request; } + // Custom validation resulting from ServiceSpecCharacteristicValue range interval and type validation (https://labs.etsi.org/rep/groups/osl/code/-/epics/30) + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addValidators(serviceSpecificationValidator); + } + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')" ) public ResponseEntity createServiceSpecification( @Parameter(description = "The ServiceSpecification to be created", required = true) @Valid @RequestBody ServiceSpecificationCreate serviceSpecification) { diff --git a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java new file mode 100644 index 00000000..1d711875 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -0,0 +1,96 @@ +package org.etsi.osl.tmf.util; + +import org.etsi.osl.tmf.common.model.ERangeInterval; +import org.etsi.osl.tmf.common.model.EValueType; +import org.etsi.osl.tmf.scm633.model.ServiceSpecCharacteristicValue; +import org.etsi.osl.tmf.scm633.model.ServiceSpecificationUpdate; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Objects; + +@Component +public class ServiceSpecificationValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return ServiceSpecificationUpdate.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + ServiceSpecificationUpdate update = (ServiceSpecificationUpdate) target; + boolean invalid = update.getServiceSpecCharacteristic().stream() + .flatMap(serviceSpecCharacteristic -> + serviceSpecCharacteristic.getServiceSpecCharacteristicValue().stream()) + .anyMatch(serviceSpecCharacteristicValue -> + !validateType(serviceSpecCharacteristicValue) || !isWithinRangeInterval(serviceSpecCharacteristicValue)); + if (invalid) { + errors.reject("invalid.request"); + } + } + + private boolean validateType(ServiceSpecCharacteristicValue serviceSpecCharacteristicValue) { + final String INTEGER_REGEX = "[-+]?\\d+"; + final String FLOAT_REGEX = "[-+]?\\d*([.,]\\d+)?([eE][-+]?\\d+)?"; + final String BOOLEAN_REGEX = "(?i)true|false"; + final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + if (serviceSpecCharacteristicValue.getValueType() == null) { + return true; + } + String value = serviceSpecCharacteristicValue.getValue().getValue(); + if (value == null || value.isBlank()) { + return true; + } + try { + return switch (EValueType.getEnum(serviceSpecCharacteristicValue.getValueType())) { + case INTEGER, SMALLINT, lONGINT -> value.matches(INTEGER_REGEX); + case FLOAT -> value.matches(FLOAT_REGEX); + case BOOLEAN -> value.matches(BOOLEAN_REGEX) || value.matches(INTEGER_REGEX); + case TIMESTAMP -> { + try { + LocalDateTime.parse(value, TIMESTAMP_FORMATTER); + yield true; + } catch (DateTimeParseException e) { + yield false; + } + } + default -> true; + }; + } catch (IllegalArgumentException e) { + return false; + } + } + + private boolean isWithinRangeInterval(ServiceSpecCharacteristicValue serviceSpecCharacteristicValue) { + if (serviceSpecCharacteristicValue.getRangeInterval() == null) { + return true; + } + if (!Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.INTEGER.getValue()) && + !Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.SMALLINT.getValue()) && + !Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.lONGINT.getValue())) { + return true; + } + String stringValue = serviceSpecCharacteristicValue.getValue().getValue(); + if (stringValue == null || stringValue.isBlank()) { + return true; + } + int valueFrom = serviceSpecCharacteristicValue.getValueFrom() != null ? serviceSpecCharacteristicValue.getValueFrom() : Integer.MIN_VALUE; + int valueTo = serviceSpecCharacteristicValue.getValueTo() != null ? serviceSpecCharacteristicValue.getValueTo() : Integer.MAX_VALUE; + try { + int value = Integer.parseInt(stringValue); + return switch (ERangeInterval.getEnum(serviceSpecCharacteristicValue.getRangeInterval())) { + case OPEN -> value > valueFrom && value < valueTo; + case CLOSED -> value >= valueFrom && value <= valueTo; + case CLOSED_BOTTOM -> value >= valueFrom && value < valueTo; + case CLOSED_TOP -> value > valueFrom && value <= valueTo; + }; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java index dea6f82a..703e2168 100644 --- a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java +++ b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java @@ -6,6 +6,7 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.List; import com.fasterxml.jackson.core.type.TypeReference; @@ -424,4 +425,76 @@ public class ServiceSpecificationApiControllerTest { return response; } + + @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) + @Test + public void testServiceSpecInvalidRangeIntervalIsBadRequest() throws Exception { + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json"); + InputStream in = new FileInputStream(serviceSpec); + String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); + ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class); + mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(serviceSpecificationCreate))) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) + @Test + public void testServiceSpecInvalidTypesIsBadRequest() throws Exception { + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json"); + InputStream in = new FileInputStream(serviceSpec); + String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); + ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class); + mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(serviceSpecificationCreate))) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) + @Test + public void testServiceSpecValidRangeIntervalIsOk() throws Exception { + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json"); + InputStream in = new FileInputStream(serviceSpec); + String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); + ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class); + String response = mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(serviceSpecificationCreate))) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS + 1); + ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class); + assertThat(responseSpec.getName()).isEqualTo("Test Spec"); + } + + @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) + @Test + public void testServiceSpecValidTypesIsOk() throws Exception { + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json"); + InputStream in = new FileInputStream(serviceSpec); + String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); + ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class); + String response = mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(serviceSpecificationCreate))) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS + 1); + ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class); + assertThat(responseSpec.getName()).isEqualTo("Test Spec"); + } } diff --git a/src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json b/src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json new file mode 100644 index 00000000..676db51f --- /dev/null +++ b/src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json @@ -0,0 +1,43 @@ +{ + "name": "Test Spec", + "description": "Test Spec example", + "version": "1.8.0", + "isBundle": false, + "attachment": [ + ], + "relatedParty": [ + ], + "resourceSpecification": [ + ], + "serviceLevelSpecification": [ + ], + "serviceSpecCharacteristic": [ + { + "name": "Port", + "configurable": true, + "description": "This attribute specifies the port number of the service", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": "closedTop", + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": 8080, + "valueTo": 8090, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "8080", + "alias": "Number" + } + } + ] + } + ] +} diff --git a/src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json b/src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json new file mode 100644 index 00000000..48d0c4d0 --- /dev/null +++ b/src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json @@ -0,0 +1,394 @@ +{ + "name": "Test Spec", + "description": "Test Spec example", + "version": "1.8.0", + "isBundle": false, + "attachment": [ + ], + "relatedParty": [ + ], + "resourceSpecification": [ + ], + "serviceLevelSpecification": [ + ], + "serviceSpecCharacteristic": [ + { + "name": "INTEGER 1", + "configurable": true, + "description": "This attribute is an integer", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "+0.1", + "alias": "" + } + } + ] + }, + { + "name": "INTEGER 2", + "configurable": true, + "description": "This attribute is an integer", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "not an integer", + "alias": "" + } + } + ] + }, + { + "name": "SMALLINT 1", + "configurable": true, + "description": "This attribute is a smallint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "SMALLINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "SMALLINT", + "validFor": null, + "value": { + "value": "-0.1", + "alias": "" + } + } + ] + }, + { + "name": "SMALLINT 2", + "configurable": true, + "description": "This attribute is a smallint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "SMALLINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "SMALLINT", + "validFor": null, + "value": { + "value": "not a smallint", + "alias": "" + } + } + ] + }, + { + "name": "LONGINT 1", + "configurable": true, + "description": "This attribute is a longint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "LONGINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "LONGINT", + "validFor": null, + "value": { + "value": "1.123456789", + "alias": "" + } + } + ] + }, + { + "name": "LONGINT 2", + "configurable": true, + "description": "This attribute is a longint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "LONGINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "LONGINT", + "validFor": null, + "value": { + "value": "not a longint", + "alias": "" + } + } + ] + }, + { + "name": "FLOAT 1", + "configurable": true, + "description": "This attribute is a float", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "FLOAT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "FLOAT", + "validFor": null, + "value": { + "value": "+-2.3", + "alias": "" + } + } + ] + }, + { + "name": "FLOAT 2", + "configurable": true, + "description": "This attribute is a float", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "FLOAT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "FLOAT", + "validFor": null, + "value": { + "value": "127.0.0.1", + "alias": "" + } + } + ] + }, + { + "name": "FLOAT 3", + "configurable": true, + "description": "This attribute is a float", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "FLOAT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "FLOAT", + "validFor": null, + "value": { + "value": "not a float", + "alias": "" + } + } + ] + }, + { + "name": "BOOLEAN", + "configurable": true, + "description": "This attribute is a boolean", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "BOOLEAN", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "BOOLEAN", + "validFor": null, + "value": { + "value": "maybe", + "alias": "" + } + } + ] + }, + { + "name": "TIMESTAMP 1", + "configurable": true, + "description": "This attribute is a timestamp", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TIMESTAMP", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TIMESTAMP", + "validFor": null, + "value": { + "value": "sunday", + "alias": "" + } + } + ] + }, + { + "name": "TIMESTAMP 2", + "configurable": true, + "description": "This attribute is a timestamp", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TIMESTAMP", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TIMESTAMP", + "validFor": null, + "value": { + "value": "13:38:01 2025/09/03", + "alias": "" + } + } + ] + }, + { + "name": "TIMESTAMP 3", + "configurable": true, + "description": "This attribute is a timestamp", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TIMESTAMP", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TIMESTAMP", + "validFor": null, + "value": { + "value": "2025-99-99 13:38:01", + "alias": "" + } + } + ] + }, + { + "name": "TIMESTAMP 4", + "configurable": true, + "description": "This attribute is a timestamp", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TIMESTAMP", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TIMESTAMP", + "validFor": null, + "value": { + "value": "2025-09-03 99:99:99", + "alias": "" + } + } + ] + } + ] +} diff --git a/src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json b/src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json new file mode 100644 index 00000000..3d88ba5c --- /dev/null +++ b/src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json @@ -0,0 +1,43 @@ +{ + "name": "Test Spec", + "description": "Test Spec example", + "version": "1.8.0", + "isBundle": false, + "attachment": [ + ], + "relatedParty": [ + ], + "resourceSpecification": [ + ], + "serviceLevelSpecification": [ + ], + "serviceSpecCharacteristic": [ + { + "name": "Port", + "configurable": true, + "description": "This attribute specifies the port number of the service", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": "closed", + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": 8080, + "valueTo": 8090, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "8080", + "alias": "Number" + } + } + ] + } + ] +} diff --git a/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json b/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json new file mode 100644 index 00000000..b6b49c9a --- /dev/null +++ b/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json @@ -0,0 +1,394 @@ +{ + "name": "Test Spec", + "description": "Test Spec example", + "version": "1.8.0", + "isBundle": false, + "attachment": [ + ], + "relatedParty": [ + ], + "resourceSpecification": [ + ], + "serviceLevelSpecification": [ + ], + "serviceSpecCharacteristic": [ + { + "name": "INTEGER 1", + "configurable": true, + "description": "This attribute is an integer", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "+0", + "alias": "" + } + } + ] + }, + { + "name": "INTEGER 2", + "configurable": true, + "description": "This attribute is an integer", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "INTEGER", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "INTEGER", + "validFor": null, + "value": { + "value": "-0", + "alias": "" + } + } + ] + }, + { + "name": "SMALLINT 1", + "configurable": true, + "description": "This attribute is a smallint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "SMALLINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "SMALLINT", + "validFor": null, + "value": { + "value": "32767", + "alias": "" + } + } + ] + }, + { + "name": "SMALLINT 2", + "configurable": true, + "description": "This attribute is a smallint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "SMALLINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "SMALLINT", + "validFor": null, + "value": { + "value": "-32768", + "alias": "" + } + } + ] + }, + { + "name": "LONGINT 1", + "configurable": true, + "description": "This attribute is a longint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "LONGINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "LONGINT", + "validFor": null, + "value": { + "value": "9223372036854775807", + "alias": "" + } + } + ] + }, + { + "name": "LONGINT 2", + "configurable": true, + "description": "This attribute is a longint", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "LONGINT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "LONGINT", + "validFor": null, + "value": { + "value": "-9223372036854775808", + "alias": "" + } + } + ] + }, + { + "name": "FLOAT 1", + "configurable": true, + "description": "This attribute is a float", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "FLOAT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "FLOAT", + "validFor": null, + "value": { + "value": "-5.0", + "alias": "" + } + } + ] + }, + { + "name": "FLOAT 2", + "configurable": true, + "description": "This attribute is a float", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "FLOAT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "FLOAT", + "validFor": null, + "value": { + "value": "1234.567890", + "alias": "" + } + } + ] + }, + { + "name": "BINARY", + "configurable": true, + "description": "This attribute is a binary", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "BINARY", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "BINARY", + "validFor": null, + "value": { + "value": "binary_text\n", + "alias": "" + } + } + ] + }, + { + "name": "BOOLEAN 1", + "configurable": true, + "description": "This attribute is a boolean", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "BOOLEAN", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "BOOLEAN", + "validFor": null, + "value": { + "value": "true", + "alias": "" + } + } + ] + }, + { + "name": "BOOLEAN 2", + "configurable": true, + "description": "This attribute is a boolean", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "BOOLEAN", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "BOOLEAN", + "validFor": null, + "value": { + "value": "0", + "alias": "" + } + } + ] + }, + { + "name": "TEXT", + "configurable": true, + "description": "This attribute is a text", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TEXT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TEXT", + "validFor": null, + "value": { + "value": "Whatever", + "alias": "" + } + } + ] + }, + { + "name": "LONGTEXT", + "configurable": true, + "description": "This attribute is a longtext", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "LONGTEXT", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "LONGTEXT", + "validFor": null, + "value": { + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer erat mi, tincidunt vel tincidunt laoreet, lacinia in diam. Vivamus arcu risus, facilisis at leo sed, tempor scelerisque ipsum. Donec non vulputate leo, ut faucibus eros. Sed dignissim vitae nisi eu lacinia. Aenean vitae massa sed orci dictum congue vitae sit amet diam. Sed ex ligula, finibus id pulvinar sit amet, posuere vel leo. Nunc scelerisque est massa. Curabitur nec ipsum feugiat, maximus ipsum sed, elementum diam. Etiam luctus fermentum dignissim. Pellentesque eget rhoncus eros. Aenean rutrum cursus ante, eu ultricies mauris bibendum at.\n\nIn hac habitasse platea dictumst. Ut maximus mattis nunc. Nunc dapibus faucibus hendrerit. In molestie, nibh id aliquet congue, ante massa luctus augue, non porttitor libero diam sollicitudin sapien. Sed malesuada faucibus finibus. Cras posuere, justo ac tempor dictum, ante arcu convallis eros, non ullamcorper metus sapien nec quam. Etiam a libero eu elit semper tempus. Aliquam odio.", + "alias": "" + } + } + ] + }, + { + "name": "TIMESTAMP", + "configurable": true, + "description": "This attribute is a timestamp", + "extensible": null, + "isUnique": true, + "maxCardinality": 1, + "minCardinality": 1, + "regex": null, + "valueType": "TIMESTAMP", + "serviceSpecCharacteristicValue": [ + { + "isDefault": true, + "rangeInterval": null, + "regex": null, + "unitOfMeasure": "N/A", + "valueFrom": null, + "valueTo": null, + "valueType": "TIMESTAMP", + "validFor": null, + "value": { + "value": "2025-09-03 13:38:01", + "alias": "" + } + } + ] + } + ] +} -- GitLab From 653d6bb075be2e7844dcead9372d61e9773e5592 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Mon, 22 Sep 2025 14:17:27 +0100 Subject: [PATCH 3/9] Fixed bug where it would throw a NullPointerException when serviceSpecCharacteristicValue value was null --- .../util/ServiceSpecificationValidator.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java index 1d711875..63da2bcf 100644 --- a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -1,5 +1,6 @@ package org.etsi.osl.tmf.util; +import org.etsi.osl.tmf.common.model.Any; import org.etsi.osl.tmf.common.model.ERangeInterval; import org.etsi.osl.tmf.common.model.EValueType; import org.etsi.osl.tmf.scm633.model.ServiceSpecCharacteristicValue; @@ -42,18 +43,22 @@ public class ServiceSpecificationValidator implements Validator { if (serviceSpecCharacteristicValue.getValueType() == null) { return true; } - String value = serviceSpecCharacteristicValue.getValue().getValue(); - if (value == null || value.isBlank()) { + Any value = serviceSpecCharacteristicValue.getValue(); + if (value == null) { + return true; + } + String stringValue = value.getValue(); + if (stringValue == null || stringValue.isBlank()) { return true; } try { return switch (EValueType.getEnum(serviceSpecCharacteristicValue.getValueType())) { - case INTEGER, SMALLINT, lONGINT -> value.matches(INTEGER_REGEX); - case FLOAT -> value.matches(FLOAT_REGEX); - case BOOLEAN -> value.matches(BOOLEAN_REGEX) || value.matches(INTEGER_REGEX); + case INTEGER, SMALLINT, lONGINT -> stringValue.matches(INTEGER_REGEX); + case FLOAT -> stringValue.matches(FLOAT_REGEX); + case BOOLEAN -> stringValue.matches(BOOLEAN_REGEX) || stringValue.matches(INTEGER_REGEX); case TIMESTAMP -> { try { - LocalDateTime.parse(value, TIMESTAMP_FORMATTER); + LocalDateTime.parse(stringValue, TIMESTAMP_FORMATTER); yield true; } catch (DateTimeParseException e) { yield false; @@ -75,6 +80,10 @@ public class ServiceSpecificationValidator implements Validator { !Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.lONGINT.getValue())) { return true; } + Any value = serviceSpecCharacteristicValue.getValue(); + if (value == null) { + return true; + } String stringValue = serviceSpecCharacteristicValue.getValue().getValue(); if (stringValue == null || stringValue.isBlank()) { return true; @@ -82,12 +91,12 @@ public class ServiceSpecificationValidator implements Validator { int valueFrom = serviceSpecCharacteristicValue.getValueFrom() != null ? serviceSpecCharacteristicValue.getValueFrom() : Integer.MIN_VALUE; int valueTo = serviceSpecCharacteristicValue.getValueTo() != null ? serviceSpecCharacteristicValue.getValueTo() : Integer.MAX_VALUE; try { - int value = Integer.parseInt(stringValue); + int intValue = Integer.parseInt(stringValue); return switch (ERangeInterval.getEnum(serviceSpecCharacteristicValue.getRangeInterval())) { - case OPEN -> value > valueFrom && value < valueTo; - case CLOSED -> value >= valueFrom && value <= valueTo; - case CLOSED_BOTTOM -> value >= valueFrom && value < valueTo; - case CLOSED_TOP -> value > valueFrom && value <= valueTo; + case OPEN -> intValue > valueFrom && intValue < valueTo; + case CLOSED -> intValue >= valueFrom && intValue <= valueTo; + case CLOSED_BOTTOM -> intValue >= valueFrom && intValue < valueTo; + case CLOSED_TOP -> intValue > valueFrom && intValue <= valueTo; }; } catch (IllegalArgumentException e) { return false; -- GitLab From 4149bbf4f5b4bfc2d938da72679a63f5ac7baf38 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Mon, 22 Sep 2025 17:06:55 +0100 Subject: [PATCH 4/9] Changed duplicated code --- .../org/etsi/osl/tmf/util/ServiceSpecificationValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java index 63da2bcf..917eef74 100644 --- a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -84,7 +84,7 @@ public class ServiceSpecificationValidator implements Validator { if (value == null) { return true; } - String stringValue = serviceSpecCharacteristicValue.getValue().getValue(); + String stringValue = value.getValue(); if (stringValue == null || stringValue.isBlank()) { return true; } -- GitLab From 60c95d872251d2d98de48b6c40d03fde2f48d607 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Fri, 10 Oct 2025 10:58:41 +0000 Subject: [PATCH 5/9] Re-added missing import that was causing build to fail --- .../api/scm633/ServiceSpecificationApiControllerTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java index dcf7e92e..8ba91bc8 100644 --- a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java +++ b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java @@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import org.apache.commons.io.IOUtils; import org.etsi.osl.services.api.BaseIT; -- GitLab From 670a7751905dcdd4547422de8c11db04481724f4 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Fri, 10 Oct 2025 11:17:09 +0000 Subject: [PATCH 6/9] Updated test assertions to not have fixed bootstrap number of service specs in the repo --- .../scm633/ServiceSpecificationApiControllerTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java index 8ba91bc8..304074d6 100644 --- a/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java +++ b/src/test/java/org/etsi/osl/services/api/scm633/ServiceSpecificationApiControllerTest.java @@ -438,7 +438,6 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) @Test public void testServiceSpecInvalidRangeIntervalIsBadRequest() throws Exception { - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json"); InputStream in = new FileInputStream(serviceSpec); String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); @@ -454,7 +453,6 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) @Test public void testServiceSpecInvalidTypesIsBadRequest() throws Exception { - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json"); InputStream in = new FileInputStream(serviceSpec); String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); @@ -470,7 +468,7 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) @Test public void testServiceSpecValidRangeIntervalIsOk() throws Exception { - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + final int existingServiceSpecs = specRepoService.findAll().size(); File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json"); InputStream in = new FileInputStream(serviceSpec); String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); @@ -482,7 +480,7 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS + 1); + assertThat(specRepoService.findAll().size()).isEqualTo(existingServiceSpecs + 1); ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class); assertThat(responseSpec.getName()).isEqualTo("Test Spec"); } @@ -490,7 +488,7 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) @Test public void testServiceSpecValidTypesIsOk() throws Exception { - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS); + final int existingServiceSpecs = specRepoService.findAll().size(); File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json"); InputStream in = new FileInputStream(serviceSpec); String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8); @@ -502,7 +500,7 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - assertThat(specRepoService.findAll().size()).isEqualTo(FIXED_BOOTSTRAPS_SPECS + 1); + assertThat(specRepoService.findAll().size()).isEqualTo(existingServiceSpecs + 1); ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class); assertThat(responseSpec.getName()).isEqualTo("Test Spec"); } -- GitLab From e4fa73970596f058eaa72a15087bc0732b5a8884 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Fri, 10 Oct 2025 15:47:12 +0000 Subject: [PATCH 7/9] Fixed NullPointerException in ServiceSpecificationValidator when list of characteristics was null --- .../org/etsi/osl/tmf/util/ServiceSpecificationValidator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java index 917eef74..ef874fef 100644 --- a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -25,6 +25,9 @@ public class ServiceSpecificationValidator implements Validator { @Override public void validate(Object target, Errors errors) { ServiceSpecificationUpdate update = (ServiceSpecificationUpdate) target; + if (update.getServiceSpecCharacteristic() == null) { + return; + } boolean invalid = update.getServiceSpecCharacteristic().stream() .flatMap(serviceSpecCharacteristic -> serviceSpecCharacteristic.getServiceSpecCharacteristicValue().stream()) -- GitLab From b0030333edd96199bc0d10dad6f9dd675445ae62 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Fri, 21 Nov 2025 17:58:12 +0000 Subject: [PATCH 8/9] Fixed datetime formatter not using ISO format --- .../org/etsi/osl/tmf/util/ServiceSpecificationValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java index ef874fef..96089022 100644 --- a/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -42,7 +42,7 @@ public class ServiceSpecificationValidator implements Validator { final String INTEGER_REGEX = "[-+]?\\d+"; final String FLOAT_REGEX = "[-+]?\\d*([.,]\\d+)?([eE][-+]?\\d+)?"; final String BOOLEAN_REGEX = "(?i)true|false"; - final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + final DateTimeFormatter ISO_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); if (serviceSpecCharacteristicValue.getValueType() == null) { return true; } @@ -61,7 +61,7 @@ public class ServiceSpecificationValidator implements Validator { case BOOLEAN -> stringValue.matches(BOOLEAN_REGEX) || stringValue.matches(INTEGER_REGEX); case TIMESTAMP -> { try { - LocalDateTime.parse(stringValue, TIMESTAMP_FORMATTER); + LocalDateTime.parse(stringValue, ISO_DATE_TIME); yield true; } catch (DateTimeParseException e) { yield false; -- GitLab From 8e416561451b89ccc7ed480a9f136f3e444b8677 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Fri, 21 Nov 2025 18:11:12 +0000 Subject: [PATCH 9/9] Updated test resource to pass ISO_DATE_TIME format --- .../reposervices/scm633/testServiceSpecValidTypes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json b/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json index b6b49c9a..e2aee03b 100644 --- a/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json +++ b/src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json @@ -384,7 +384,7 @@ "valueType": "TIMESTAMP", "validFor": null, "value": { - "value": "2025-09-03 13:38:01", + "value": "2045-06-30T14:52:18.783+0100", "alias": "" } } -- GitLab