Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
* org.opentest4j.AssertionFailedError} with expected and actual fields preserved via {@link
* FailureCollector#rewrap(String, AssertionError)}.
*
* <p>The {@code path} field on the assertion follows the same {@code response.*} convention as
* {@link JsonSchemaAssertionEvaluator}, resolved via {@link ResponseValueExtractor}:
*
* <ul>
* <li>{@code response.body.json} — compare the entire parsed JSON body
* <li>{@code response.body.json.<jsonpath>} — compare only the value at the given JSONPath
* expression
* </ul>
*
* <p>Current limitation: only top-level field names are supported in the ignore list. Nested
* JSONPath expressions are not yet evaluated.
*/
Expand Down Expand Up @@ -110,8 +119,22 @@ public void evaluate(ApiResponse response, FailureCollector collector) {
return;
}

JsonNode actualNode;
switch (ResponseValueExtractor.extract(response, assertion.path())) {
case ResponseValueExtractor.Result.Found found -> actualNode = objectMapper.valueToTree(found.value());
case ResponseValueExtractor.Result.Missing missing -> {
collector.fail(
String.format("Path '%s' not found in response for json_match assertion", missing.path()),
null);
return;
}
case ResponseValueExtractor.Result.Error error -> {
collector.fail(error.message(), null);
return;
}
}

try {
JsonNode actualNode = objectMapper.valueToTree(response.body().json());
JsonNode expectedNode = objectMapper.readTree(expectedJson);

removeIgnoredFields(actualNode, assertion.expected().ignore());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,22 @@ private static JsonMatchAssertionEvaluator evaluator(String expectedContent, Lis

private static JsonMatchAssertionEvaluator evaluator(
String expectedContent, List<String> ignore, Map<String, String> suiteVars, Map<String, String> testVars) {
return evaluatorForPath("response.body.json", expectedContent, ignore, suiteVars, testVars);
}

private static JsonMatchAssertionEvaluator evaluatorForPath(
String path, String expectedContent, List<String> ignore) {
return evaluatorForPath(path, expectedContent, ignore, Map.of(), Map.of());
}

private static JsonMatchAssertionEvaluator evaluatorForPath(
String path,
String expectedContent,
List<String> ignore,
Map<String, String> suiteVars,
Map<String, String> testVars) {
ObjectExpectedValue expected = new ObjectExpectedValue("inline", expectedContent, ignore);
JsonMatchAssertion assertion = new JsonMatchAssertion("response.body.json", expected);
JsonMatchAssertion assertion = new JsonMatchAssertion(path, expected);
return new JsonMatchAssertionEvaluator(
assertion, null, OBJECT_MAPPER, Map.of("suite", suiteVars, "test", testVars));
}
Expand Down Expand Up @@ -201,4 +215,77 @@ void nullSuiteDirWithFileReferenceThrowsIllegalState() {
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Suite directory is required");
}

@Test
void jsonPathSubexpressionExtractsNestedObject() {
String text = "{\"data\":{\"id\":1}}";
Object json = Map.of("data", Map.of("id", 1));

FailureCollector collector = new FailureCollector();
evaluatorForPath("response.body.json.$.data", "{\"id\":1}", List.of())
.evaluate(responseWithJson(text, json), collector);

assertThatCode(collector::assertAll).doesNotThrowAnyException();
}

@Test
void jsonPathArrayElementExtractionMatchesIssueExample() {
String text = "[{\"data\":{\"id\":1}}]";
Object json = List.of(Map.of("data", Map.of("id", 1)));

FailureCollector collector = new FailureCollector();
evaluatorForPath("response.body.json.$[0].data", "{\"id\":1}", List.of())
.evaluate(responseWithJson(text, json), collector);

assertThatCode(collector::assertAll).doesNotThrowAnyException();
}

@Test
void jsonPathSubexpressionMismatchFails() {
String text = "{\"data\":{\"id\":1}}";
Object json = Map.of("data", Map.of("id", 1));

FailureCollector collector = new FailureCollector();
evaluatorForPath("response.body.json.$.data", "{\"id\":2}", List.of())
.evaluate(responseWithJson(text, json), collector);

assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
}

@Test
void missingJsonPathRecordsFailure() {
String text = "{\"data\":{\"id\":1}}";
Object json = Map.of("data", Map.of("id", 1));

FailureCollector collector = new FailureCollector();
evaluatorForPath("response.body.json.$.nonexistent", "{\"id\":1}", List.of())
.evaluate(responseWithJson(text, json), collector);

assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
}

@Test
void invalidJsonPathRecordsFailure() {
String text = "{\"data\":{\"id\":1}}";
Object json = Map.of("data", Map.of("id", 1));

FailureCollector collector = new FailureCollector();
evaluatorForPath("response.body.json.$[", "{\"id\":1}", List.of())
.evaluate(responseWithJson(text, json), collector);

assertThatThrownBy(collector::assertAll).isInstanceOf(MultipleFailuresError.class);
}

@Test
void unsupportedPathPrefixRecordsFailure() {
String text = "{\"data\":{\"id\":1}}";
Object json = Map.of("data", Map.of("id", 1));

FailureCollector collector = new FailureCollector();
evaluatorForPath("foo.bar", "{\"id\":1}", List.of()).evaluate(responseWithJson(text, json), collector);

assertThatThrownBy(collector::assertAll)
.isInstanceOf(MultipleFailuresError.class)
.hasMessageContaining("response.");
}
}
Loading