From 3c978fb4c5fe0e2f81fb83754d34554c93f5cce3 Mon Sep 17 00:00:00 2001 From: Michal Slowik Date: Sun, 28 Jun 2026 16:24:26 +0200 Subject: [PATCH] header context for response validation --- .../interaction/response/ResponseValidator.java | 7 +++++-- .../report/ImmutableMessageContext.java | 7 +++++++ .../oai/validator/report/ValidationReport.java | 12 ++++++++++++ .../OpenAPIV3ResponseValidationTest.java | 14 ++++++++++++-- .../oai/validator/util/ValidatorTestUtil.java | 16 +++++++++++++--- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/interaction/response/ResponseValidator.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/interaction/response/ResponseValidator.java index 931f5321..e7de16e8 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/interaction/response/ResponseValidator.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/interaction/response/ResponseValidator.java @@ -242,16 +242,19 @@ private ValidationReport validateHeader(final ApiOperation apiOperation, final String headerName, final Header apiHeader, final Collection propertyValues) { + final ValidationReport.MessageContext context = + ValidationReport.MessageContext.create().withResponseHeaderName(headerName).build(); if (propertyValues.isEmpty() && TRUE.equals(apiHeader.getRequired())) { return ValidationReport.singleton( messages.get("validation.response.header.missing", headerName, apiOperation.getApiPath().original()) - ); + ).withAdditionalContext(context); } return propertyValues .stream() - .map(v -> schemaValidator.validate(v, apiHeader.getSchema(), "response.header")) + .map(v -> schemaValidator.validate(v, apiHeader.getSchema(), "response.header") + .withAdditionalContext(context)) .reduce(ValidationReport.empty(), ValidationReport::merge); } diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ImmutableMessageContext.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ImmutableMessageContext.java index 241a0732..9e962aec 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ImmutableMessageContext.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ImmutableMessageContext.java @@ -22,6 +22,7 @@ class ImmutableMessageContext implements ValidationReport.MessageContext { private final String apiRequestContentType; private final Integer responseStatus; + private final String responseHeaderName; private final ApiResponse apiResponseDefinition; private final Location location; @@ -38,6 +39,7 @@ class ImmutableMessageContext implements ValidationReport.MessageContext { apiRequestBodyDefinition = builder.apiRequestBodyDefinition; apiRequestContentType = builder.apiRequestContentType; responseStatus = builder.responseStatus; + responseHeaderName = builder.responseHeaderName; apiResponseDefinition = builder.apiResponse; location = builder.location; whitelistRule = builder.whitelistRule; @@ -81,6 +83,11 @@ public Optional getResponseStatus() { return Optional.ofNullable(responseStatus); } + @Override + public Optional getHResponseHeaderName() { + return Optional.ofNullable(responseHeaderName); + } + @JsonIgnore @Override public Optional getApiResponseDefinition() { diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ValidationReport.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ValidationReport.java index 96deb7c7..36e74558 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ValidationReport.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/report/ValidationReport.java @@ -184,6 +184,8 @@ static Builder from(final MessageContext other) { Optional getResponseStatus(); + Optional getHResponseHeaderName(); + Optional getApiResponseDefinition(); /** @@ -225,6 +227,7 @@ class Builder { RequestBody apiRequestBodyDefinition; Integer responseStatus; + String responseHeaderName; ApiResponse apiResponse; Location location; @@ -244,6 +247,7 @@ private Builder(final MessageContext init) { apiRequestBodyDefinition = init.getApiRequestBodyDefinition().orElse(null); apiRequestContentType = init.getApiRequestContentType().orElse(null); responseStatus = init.getResponseStatus().orElse(null); + responseHeaderName = init.getHResponseHeaderName().orElse(null); apiResponse = init.getApiResponseDefinition().orElse(null); location = init.getLocation().orElse(null); whitelistRule = init.getAppliedWhitelistRule().orElse(null); @@ -285,6 +289,11 @@ public Builder withResponseStatus(final Integer status) { return this; } + public Builder withResponseHeaderName(final String headerName) { + responseHeaderName = headerName; + return this; + } + public Builder withApiResponseDefinition(final ApiResponse apiResponseDefinition) { apiResponse = apiResponseDefinition; return this; @@ -327,6 +336,9 @@ public Builder withAdditionalDataFrom(final MessageContext other) { if (responseStatus == null) { responseStatus = other.getResponseStatus().orElse(null); } + if (responseHeaderName == null) { + responseHeaderName = other.getHResponseHeaderName().orElse(null); + } if (apiResponse == null) { apiResponse = other.getApiResponseDefinition().orElse(null); } diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/OpenAPIV3ResponseValidationTest.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/OpenAPIV3ResponseValidationTest.java index bf930ac2..3df6facb 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/OpenAPIV3ResponseValidationTest.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/OpenAPIV3ResponseValidationTest.java @@ -20,6 +20,9 @@ import static com.atlassian.oai.validator.util.ValidatorTestUtil.assertPass; import static com.atlassian.oai.validator.util.ValidatorTestUtil.loadJsonResponse; import static com.atlassian.oai.validator.util.ValidatorTestUtil.loadXmlResponse; +import static com.atlassian.oai.validator.util.ValidatorTestUtil.getContextsForKey; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; public class OpenAPIV3ResponseValidationTest { @@ -204,8 +207,15 @@ public void validate_withInvalidResponseHeader_shouldFail() { .withHeader("X-Failure-Code", "1.0") .build(); - assertFail(classUnderTest.validateResponse("/healthcheck", GET, response), - "validation.response.header.schema.type"); + final ValidationReport report = classUnderTest.validateResponse("/healthcheck", GET, response); + + assertFail(report, "validation.response.header.schema.type"); + getContextsForKey(report, "validation.response.header.schema.type").forEach(ctx -> { + assertThat(ctx.isPresent(), is(true)); + assertThat(ctx.get().getHResponseHeaderName().isPresent(), is(true)); + assertThat(ctx.get().getHResponseHeaderName().get(), is("X-Failure-Code")); + } + ); } @Test diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/util/ValidatorTestUtil.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/util/ValidatorTestUtil.java index 0b20dfce..ab2a04f7 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/util/ValidatorTestUtil.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/util/ValidatorTestUtil.java @@ -9,10 +9,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import static java.lang.String.format; -import static java.util.stream.Collectors.toList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; @@ -51,10 +51,10 @@ private static void assertFail(final ValidationReport report, final boolean expe log.trace(JsonValidationReportFormat.getInstance().apply(report)); assertThat("Expected validation errors but found none. Enable trace logging for more details.", report.getMessages(), is(not(empty()))); - final List foundKeys = report.getMessages().stream().map(ValidationReport.Message::getKey).collect(toList()); + final List foundKeys = report.getMessages().stream().map(ValidationReport.Message::getKey).toList(); for (final String key : expectedKeys) { - assertThat(format("Expected message key '%s' but not found. Found <%s>.", key, foundKeys.toString()), + assertThat(format("Expected message key '%s' but not found. Found <%s>.", key, foundKeys), foundKeys.contains(key), is(true)); } @@ -83,6 +83,16 @@ public static Consumer assertPass() { return ValidatorTestUtil::assertPass; } + /** + * Returns existing (Optional.isPresent) contexts for messages in {@code report} for message key = {@code key}. + */ + public static List> getContextsForKey(final ValidationReport report, final String key) { + return report.getMessages().stream() + .filter(v -> key.equals(v.getKey())) + .map(ValidationReport.Message::getContext) + .toList(); + } + /** * Load a response JSON file with the given name. *