diff --git a/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java index 41800da9..ac730ba8 100644 --- a/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java +++ b/openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java @@ -3,6 +3,7 @@ import com.atlassian.oai.validator.model.Request; import com.atlassian.oai.validator.model.SimpleRequest; import com.atlassian.oai.validator.model.SimpleResponse; +import com.getyourguide.openapi.validation.api.log.LogLevel; import com.getyourguide.openapi.validation.api.log.OpenApiViolationHandler; import com.getyourguide.openapi.validation.api.metrics.MetricsReporter; import com.getyourguide.openapi.validation.api.model.Direction; @@ -98,7 +99,7 @@ public List validateRequestObject( var result = validator.validateRequest(simpleRequest); var violations = mapper.map(result, request, response, Direction.REQUEST, requestBody); return violations.stream() - .filter(violation -> !violationExclusions.isExcluded(violation)) + .filter(this::isNonExcludedViolation) .toList(); } catch (Exception e) { log.error("[OpenAPI Validation] Could not validate request", e); @@ -145,11 +146,15 @@ public List validateResponseObject( ); var violations = mapper.map(result, request, response, Direction.RESPONSE, responseBody); return violations.stream() - .filter(violation -> !violationExclusions.isExcluded(violation)) + .filter(this::isNonExcludedViolation) .toList(); } catch (Exception e) { log.error("[OpenAPI Validation] Could not validate response", e); return List.of(); } } + + private boolean isNonExcludedViolation(OpenApiViolation violation) { + return !LogLevel.IGNORE.equals(violation.getLevel()) && !violationExclusions.isExcluded(violation); + } } diff --git a/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java b/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java index 9b2e4882..652a5c2c 100644 --- a/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java +++ b/openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java @@ -8,8 +8,10 @@ import com.atlassian.oai.validator.model.SimpleRequest; import com.atlassian.oai.validator.report.ValidationReport; +import com.getyourguide.openapi.validation.api.log.LogLevel; import com.getyourguide.openapi.validation.api.model.OpenApiViolation; import com.getyourguide.openapi.validation.api.model.RequestMetaData; +import com.getyourguide.openapi.validation.api.model.ResponseMetaData; import com.getyourguide.openapi.validation.core.exclusions.InternalViolationExclusions; import com.getyourguide.openapi.validation.core.mapper.ValidationReportToOpenApiViolationsMapper; import com.getyourguide.openapi.validation.core.validator.OpenApiInteractionValidatorWrapper; @@ -19,6 +21,8 @@ import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -51,49 +55,178 @@ public void setup() { } @Test + @DisplayName("When thread pool executor rejects execution then it should not throw") public void testWhenThreadPoolExecutorRejectsExecutionThenItShouldNotThrow() { Mockito.doThrow(new RejectedExecutionException()).when(executor).execute(any()); openApiRequestValidator.validateRequestObjectAsync(mock(), null, null, mock()); } - @Test - public void testWhenEncodedQueryParamIsPassedThenValidationShouldHappenWithQueryParamDecoded() { - var uri = URI.create("https://api.example.com?ids=1%2C2%2C3&text=e%3Dmc2%20%26%20more&spaces=this+is+a+sparta"); - var request = new RequestMetaData("GET", uri, new HashMap<>()); + @Nested + @DisplayName("validateRequestObject") + public class ValidateRequestObjectTests { + + @Test + @DisplayName("When encoded query param is passed then validation should happen with query param decoded") + public void testWhenEncodedQueryParamIsPassedThenValidationShouldHappenWithQueryParamDecoded() { + var uri = URI.create("https://api.example.com?ids=1%2C2%2C3&text=e%3Dmc2%20%26%20more&spaces=this+is+a+sparta"); + var request = new RequestMetaData("GET", uri, new HashMap<>()); + + openApiRequestValidator.validateRequestObject(request, null); + + var simpleRequestArgumentCaptor = ArgumentCaptor.forClass(SimpleRequest.class); + verify(validator).validateRequest(simpleRequestArgumentCaptor.capture()); + verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "ids", "1,2,3"); + verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "text", "e=mc2 & more"); + verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "spaces", "this is a sparta"); + } + + @Test + @DisplayName("When violation is excluded then it should not be returned") + public void testWhenViolationIsExcludedThenItShouldNotBeReturned() { + var validationReport = mock(ValidationReport.class); + when(validator.validateRequest(any())).thenReturn(validationReport); + var violationExcluded = mock(OpenApiViolation.class); + var violations = List.of(violationExcluded, mock(OpenApiViolation.class)); + when(mapper.map(any(), any(), any(), any(), any())).thenReturn(violations); + when(internalViolationExclusions.isExcluded(violationExcluded)).thenReturn(true); + + var result = openApiRequestValidator.validateRequestObject(createRequest(), null); + + assertEquals(1, result.size()); + assertEquals(violations.get(1), result.getFirst()); + } + + @Test + @DisplayName("When violation has log level IGNORE then it should not be returned") + public void testWhenRequestViolationHasLogLevelIgnoreThenItShouldNotBeReturned() { + var violationIgnored = createViolation(LogLevel.IGNORE); + var violationError = createViolation(LogLevel.ERROR); + mockRequestValidation(List.of(violationIgnored, violationError)); + + var result = openApiRequestValidator.validateRequestObject(createRequest(), null); + + assertSingleViolationReturned(result, violationError); + } + + @Test + @DisplayName("When violation has log level IGNORE and another is excluded then both should not be returned") + public void testWhenRequestViolationHasLogLevelIgnoreAndIsExcludedThenItShouldNotBeReturned() { + var violationIgnored = createViolation(LogLevel.IGNORE); + var violationExcluded = createViolation(LogLevel.WARN); + when(internalViolationExclusions.isExcluded(violationExcluded)).thenReturn(true); + var violationError = createViolation(LogLevel.ERROR); + mockRequestValidation(List.of(violationIgnored, violationExcluded, violationError)); + + var result = openApiRequestValidator.validateRequestObject(createRequest(), null); + + assertSingleViolationReturned(result, violationError); + } + + @Test + @DisplayName("When all violations are ignored then empty list is returned") + public void testWhenAllRequestViolationsAreIgnoredThenEmptyListIsReturned() { + var violation1 = createViolation(LogLevel.IGNORE); + var violation2 = createViolation(LogLevel.IGNORE); + mockRequestValidation(List.of(violation1, violation2)); + + var result = openApiRequestValidator.validateRequestObject(createRequest(), null); + + assertNoViolationsReturned(result); + } + } + + @Nested + @DisplayName("validateResponseObject") + public class ValidateResponseObjectTests { + + @Test + @DisplayName("When violation has log level IGNORE then it should not be returned") + public void testWhenResponseViolationHasLogLevelIgnoreThenItShouldNotBeReturned() { + var violationIgnored = createViolation(LogLevel.IGNORE); + var violationWarn = createViolation(LogLevel.WARN); + mockResponseValidation(List.of(violationIgnored, violationWarn)); + + var result = executeValidateResponseObject(); + + assertSingleViolationReturned(result, violationWarn); + } + + @Test + @DisplayName("When violation has log level IGNORE and another is excluded then both should not be returned") + public void testWhenResponseViolationHasLogLevelIgnoreAndIsExcludedThenItShouldNotBeReturned() { + var violationIgnored = createViolation(LogLevel.IGNORE); + var violationExcluded = createViolation(LogLevel.INFO); + when(internalViolationExclusions.isExcluded(violationExcluded)).thenReturn(true); + var violationError = createViolation(LogLevel.ERROR); + mockResponseValidation(List.of(violationIgnored, violationExcluded, violationError)); + + var result = executeValidateResponseObject(); + + assertSingleViolationReturned(result, violationError); + } + + @Test + @DisplayName("When all violations are ignored then empty list is returned") + public void testWhenAllResponseViolationsAreIgnoredThenEmptyListIsReturned() { + var violation1 = createViolation(LogLevel.IGNORE); + var violation2 = createViolation(LogLevel.IGNORE); + mockResponseValidation(List.of(violation1, violation2)); + + var result = executeValidateResponseObject(); + + assertNoViolationsReturned(result); + } + + private List executeValidateResponseObject() { + var request = createRequest(); + var response = createResponse(); + return openApiRequestValidator.validateResponseObject(request, response, null); + } + } - openApiRequestValidator.validateRequestObject(request, null); + private void verifyQueryParamValueEquals( + ArgumentCaptor simpleRequestArgumentCaptor, + String name, + String expected + ) { + var ids = simpleRequestArgumentCaptor.getValue().getQueryParameterValues(name).iterator().next(); + assertEquals(expected, ids); + } - var simpleRequestArgumentCaptor = ArgumentCaptor.forClass(SimpleRequest.class); - verify(validator).validateRequest(simpleRequestArgumentCaptor.capture()); - verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "ids", "1,2,3"); - verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "text", "e=mc2 & more"); - verifyQueryParamValueEquals(simpleRequestArgumentCaptor, "spaces", "this is a sparta"); + private OpenApiViolation createViolation(LogLevel level) { + var violation = mock(OpenApiViolation.class); + when(violation.getLevel()).thenReturn(level); + return violation; } - @Test - public void testWhenViolationIsExcludedThenItShouldNotBeReturned() { + private RequestMetaData createRequest() { var uri = URI.create("https://api.example.com/path"); - var request = new RequestMetaData("GET", uri, new HashMap<>()); + return new RequestMetaData("GET", uri, new HashMap<>()); + } + + private ResponseMetaData createResponse() { + return new ResponseMetaData(200, "application/json", new HashMap<>()); + } + + private void mockRequestValidation(List violations) { var validationReport = mock(ValidationReport.class); when(validator.validateRequest(any())).thenReturn(validationReport); - var violationExcluded = mock(OpenApiViolation.class); - var violations = List.of(violationExcluded, mock(OpenApiViolation.class)); when(mapper.map(any(), any(), any(), any(), any())).thenReturn(violations); - when(internalViolationExclusions.isExcluded(violationExcluded)).thenReturn(true); + } - var result = openApiRequestValidator.validateRequestObject(request, null); + private void mockResponseValidation(List violations) { + var validationReport = mock(ValidationReport.class); + when(validator.validateResponse(any(), any(), any())).thenReturn(validationReport); + when(mapper.map(any(), any(), any(), any(), any())).thenReturn(violations); + } + private void assertSingleViolationReturned(List result, OpenApiViolation expected) { assertEquals(1, result.size()); - assertEquals(violations.get(1), result.getFirst()); + assertEquals(expected, result.getFirst()); } - private void verifyQueryParamValueEquals( - ArgumentCaptor simpleRequestArgumentCaptor, - String name, - String expected - ) { - var ids = simpleRequestArgumentCaptor.getValue().getQueryParameterValues(name).iterator().next(); - assertEquals(expected, ids); + private void assertNoViolationsReturned(List result) { + assertEquals(0, result.size()); } } diff --git a/spring-boot-starter/spring-boot-starter-web/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java b/spring-boot-starter/spring-boot-starter-web/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java index f29c4369..05043e9f 100644 --- a/spring-boot-starter/spring-boot-starter-web/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java +++ b/spring-boot-starter/spring-boot-starter-web/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java @@ -20,8 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; @@ -39,7 +39,7 @@ public class FailOnViolationIntegrationTest { @Autowired private TestViolationLogger openApiViolationLogger; - @SpyBean + @MockitoSpyBean private DefaultRestController defaultRestController; @BeforeEach diff --git a/spring-boot-starter/spring-boot-starter-webflux/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java b/spring-boot-starter/spring-boot-starter-webflux/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java index 602236b3..f110c512 100644 --- a/spring-boot-starter/spring-boot-starter-webflux/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java +++ b/spring-boot-starter/spring-boot-starter-webflux/src/test/java/com/getyourguide/openapi/validation/integration/FailOnViolationIntegrationTest.java @@ -14,8 +14,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; @@ -33,7 +33,7 @@ public class FailOnViolationIntegrationTest { @Autowired private TestViolationLogger openApiViolationLogger; - @SpyBean + @MockitoSpyBean private DefaultRestController defaultRestController; @BeforeEach