Commit d4ec5209 authored by Diogo Santos's avatar Diogo Santos
Browse files

Implemented range interval validation and type validation for Service...

Implemented range interval validation and type validation for Service Specification request body, plus 4 new tests
parent 04bb9c82
Loading
Loading
Loading
Loading
Loading
+19 −10
Original line number Diff line number Diff line
@@ -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;
@@ -18,15 +19,23 @@ public class RestExceptionHandler extends ResponseEntityExceptionHandler {
	@Override
	protected ResponseEntity<Object> 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<Object>(apiError, apiError.getStatus());
		return buildResponseEntity(ex);
	}

	// other exception handlers below

	private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
		return new ResponseEntity<>(apiError, apiError.getStatus());
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(
			MethodArgumentNotValidException ex,
			HttpHeaders headers,
			HttpStatusCode status,
			WebRequest request) {
		return buildResponseEntity(ex);
	}
	// other exception handlers below

	private ResponseEntity<Object> buildResponseEntity(Throwable ex) {
		String error = "Malformed JSON request";
		ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, error, ex);
		return new ResponseEntity<Object>(apiError, apiError.getStatus());
	}
}
+12 −8
Original line number Diff line number Diff line
@@ -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<ServiceSpecification> createServiceSpecification(
			@Parameter(description = "The ServiceSpecification to be created", required = true) @Valid @RequestBody ServiceSpecificationCreate serviceSpecification) {
+96 −0
Original line number Diff line number Diff line
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;
        }
    }
}
+73 −0
Original line number Diff line number Diff line
@@ -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");
    }
}
+43 −0
Original line number Diff line number Diff line
{
	"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"
					}
				}
			]
		}
	]
}
Loading