diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d5da543..9b1d0036 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,6 +27,13 @@ jobs: uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 with: arguments: sourcesJar javadocJar + - name: Get tag + shell: bash + id: get_tag + run: | + TAG=${GITHUB_REF#refs/tags/} + echo "Github tag: $TAG" + echo "gh_tag=${TAG#v}" >> "$GITHUB_OUTPUT" - name: Publish to MavenCentral uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 with: @@ -38,3 +45,4 @@ jobs: SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + GH_TAG: "${{ steps.get_tag.outputs.gh_tag }}" diff --git a/RELEASE.md b/RELEASE.md index af7792cd..d3eec587 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,9 +1,17 @@ # Release process -1. Update the `version` in `build.gradle` - - Push this to `main` (optionally with a PR) -2. Create a new release on github: [direct link](https://github.com/getyourguide/openapi-validation-java/releases/new) - - Choose a tag `v....` (the new version) - - Select "Create new tag" - - Press "Generate release notes" - - Publish release +- Create a new release on github: [direct link](https://github.com/getyourguide/openapi-validation-java/releases/new) +- Choose a tag `v....` (the new version) +- Select "Create new tag" +- Press "Generate release notes" +- Publish release + +## Release SNAPSHOT version from branch + +- Create a new release on github: [direct link](https://github.com/getyourguide/openapi-validation-java/releases/new) +- Choose a tag `v....` (the new version) +- Select "Create new tag" +- **Choose your branch as target** +- Press "Generate release notes" +- **Check "Set as a pre-release"** +- Publish release diff --git a/build.gradle b/build.gradle index be00b8d2..b853cce8 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.nexus.publish) } -ext['spring-framework.version'] = '6.2.10' +ext['spring-framework.version'] = '6.2.11' ext['tomcat.version'] = '11.0.10' ext['netty.version'] = '4.2.6.Final' // Due to security vulnerabilities in 4.125.Final and older @@ -12,7 +12,8 @@ apply from: "${rootDir}/gradle/publish-root.gradle" allprojects { group = 'com.getyourguide.openapi.validation' description = 'OpenAPI Validation library' - version = '3.3.2' + // Use version from GitHub tag if provided, otherwise use default version + version = System.getenv('GH_TAG') ? System.getenv('GH_TAG').replaceFirst('^v', '') : '0-SNAPSHOT' java { toolchain { @@ -70,19 +71,19 @@ subprojects { // Security constraints constraints { - implementation("org.springframework:spring-web:6.2.10") { - because("versions below 6.2.8 have security vulnerabilities including CVE-2024-38820 - see dependabot #12") + implementation("org.springframework:spring-web:6.2.12") { + because("versions below 6.2.11 have security vulnerabilities including CVE-2024-38820 and CVE-2025-41249 - see dependabot #12, #24") } - implementation("org.springframework:spring-webmvc:6.2.10") { - because("versions below 6.2.10 have Path Traversal Vulnerability CVE-2025-41242 - see dependabot #247") + implementation("org.springframework:spring-webmvc:6.2.12") { + because("versions below 6.2.11 have security vulnerabilities including CVE-2025-41242 and CVE-2025-41249 - see dependabot #24, #247") } - implementation("org.apache.tomcat.embed:tomcat-embed-core:11.0.10") { + implementation("org.apache.tomcat.embed:tomcat-embed-core:11.0.13") { because("versions below 10.1.42 have security vulnerabilities including CVE-2024-56337 - see dependabot #13") } - implementation("org.apache.commons:commons-lang3:3.18.0") { + implementation("org.apache.commons:commons-lang3:3.19.0") { because("versions below 3.18.0 have security vulnerabilities including CVE-2025-48924 - see dependabot #15") } - implementation("io.projectreactor.netty:reactor-netty-http:1.2.9") { + implementation("io.projectreactor.netty:reactor-netty-http:1.2.11") { because("versions below 1.2.8 have security vulnerabilities including CVE-2025-22227 - see dependabot #16") } implementation("io.netty:netty-codec-http2:4.2.6.Final") { diff --git a/examples/example-spring-boot-starter-web/build.gradle b/examples/example-spring-boot-starter-web/build.gradle index e4e6a749..aa15ec56 100644 --- a/examples/example-spring-boot-starter-web/build.gradle +++ b/examples/example-spring-boot-starter-web/build.gradle @@ -6,13 +6,14 @@ plugins { } // Needed for security. See: +// - https://github.com/getyourguide/openapi-validation-java/security/dependabot/25 // - https://github.com/getyourguide/openapi-validation-java/security/dependabot/7 // - https://github.com/getyourguide/openapi-validation-java/security/dependabot/6 // Hopefully with spring-boot 3.4.2+ this won't be needed anymore and can be removed. dependencyManagement { dependencies { - dependency 'ch.qos.logback:logback-core:1.5.18' - dependency 'ch.qos.logback:logback-classic:1.5.18' + dependency 'ch.qos.logback:logback-core:1.5.19' + dependency 'ch.qos.logback:logback-classic:1.5.19' } } diff --git a/examples/example-spring-boot-starter-webflux/build.gradle b/examples/example-spring-boot-starter-webflux/build.gradle index 60e1117d..26ae51a4 100644 --- a/examples/example-spring-boot-starter-webflux/build.gradle +++ b/examples/example-spring-boot-starter-webflux/build.gradle @@ -6,13 +6,14 @@ plugins { } // Needed for security. See: +// - https://github.com/getyourguide/openapi-validation-java/security/dependabot/25 // - https://github.com/getyourguide/openapi-validation-java/security/dependabot/7 // - https://github.com/getyourguide/openapi-validation-java/security/dependabot/6 // Hopefully with spring-boot 3.4.2+ this won't be needed anymore and can be removed. dependencyManagement { dependencies { - dependency 'ch.qos.logback:logback-core:1.5.18' - dependency 'ch.qos.logback:logback-classic:1.5.18' + dependency 'ch.qos.logback:logback-core:1.5.19' + dependency 'ch.qos.logback:logback-classic:1.5.19' } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22294ec7..8979488f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] java = "21" -spring-boot = "3.5.5" +spring-boot = "3.5.6" spring-dependency-management = "1.1.7" -openapi-generator = "7.15.0" +openapi-generator = "7.16.0" openapi-tools = "0.2.7" -swagger = "2.2.36" +swagger = "2.2.39" swagger-request-validator = "2.45.1" jakarta-validation = "3.1.1" -lombok = "1.18.40" +lombok = "1.18.42" commons-codec = "1.19.0" find-bugs = "3.0.2" gradle-nexus-publish-plugin = "2.0.0" @@ -17,7 +17,7 @@ checkstyle = "8.44" pmd = "7.14.0" jacoco = "0.8.13" # Testing -mockito = "5.19.0" +mockito = "5.20.0" junit-jupiter = "5.13.4" junit-platform = "1.13.4" 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