diff --git a/.gitignore b/.gitignore index f91fa9371fecdbfdea0875063f0030a632267f54..e545fd0dd1f11a2fd555b28b49c5df2d7957580f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.classpath /.settings /org.etsi.osl.tmf.api.iml +/.idea/ 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 42aac3140aaa5105f903d2b9c271f92adfa2a92c..234424589dff29343569ca3e7456586124bdafb6 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 12482fb6c40803c28239eb73944481422946567b..d8944f1e046c446b6866827ead52fce04d743ca9 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 0000000000000000000000000000000000000000..9608902282fc0379b56e4018024e6899eef58d91 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/util/ServiceSpecificationValidator.java @@ -0,0 +1,108 @@ +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; +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; + if (update.getServiceSpecCharacteristic() == null) { + return; + } + 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 ISO_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + if (serviceSpecCharacteristicValue.getValueType() == null) { + return true; + } + 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 -> 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(stringValue, ISO_DATE_TIME); + 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; + } + Any value = serviceSpecCharacteristicValue.getValue(); + if (value == null) { + return true; + } + String stringValue = value.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 intValue = Integer.parseInt(stringValue); + return switch (ERangeInterval.getEnum(serviceSpecCharacteristicValue.getRangeInterval())) { + 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; + } + } +} 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 976319922d052a8b49aa9c4378eb9c2ff38e67af..304074d685fb308a524c9ed4060c76fa22163ff4 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; @@ -433,4 +434,74 @@ public class ServiceSpecificationApiControllerTest extends BaseIT { return response; } + + @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" }) + @Test + public void testServiceSpecInvalidRangeIntervalIsBadRequest() throws Exception { + 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 { + 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 { + 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); + 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(existingServiceSpecs + 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 { + 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); + 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(existingServiceSpecs + 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 0000000000000000000000000000000000000000..676db51ff10dce8927433ae54ce15ddb658234f0 --- /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 0000000000000000000000000000000000000000..48d0c4d069c7142b7f4a877057b415b61244e5e4 --- /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 0000000000000000000000000000000000000000..3d88ba5c2c483c74331784d5e90a48f107e6fde4 --- /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 0000000000000000000000000000000000000000..e2aee03be7b225df3ea21e3f487bafc096205271 --- /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": "2045-06-30T14:52:18.783+0100", + "alias": "" + } + } + ] + } + ] +}