diff --git a/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java b/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java index 82dc216..1a59724 100644 --- a/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java +++ b/src/main/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluator.java @@ -48,6 +48,15 @@ * org.opentest4j.AssertionFailedError} with expected and actual fields preserved via {@link * FailureCollector#rewrap(String, AssertionError)}. * + *

The {@code path} field on the assertion follows the same {@code response.*} convention as + * {@link JsonSchemaAssertionEvaluator}, resolved via {@link ResponseValueExtractor}: + * + *

+ * *

Current limitation: only top-level field names are supported in the ignore list. Nested * JSONPath expressions are not yet evaluated. */ @@ -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()); diff --git a/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java b/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java index fa8d014..9da9cca 100644 --- a/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java +++ b/src/test/java/io/github/snytkine/apitester/api_tester_cli/service/assertion/JsonMatchAssertionEvaluatorTest.java @@ -45,8 +45,22 @@ private static JsonMatchAssertionEvaluator evaluator(String expectedContent, Lis private static JsonMatchAssertionEvaluator evaluator( String expectedContent, List ignore, Map suiteVars, Map testVars) { + return evaluatorForPath("response.body.json", expectedContent, ignore, suiteVars, testVars); + } + + private static JsonMatchAssertionEvaluator evaluatorForPath( + String path, String expectedContent, List ignore) { + return evaluatorForPath(path, expectedContent, ignore, Map.of(), Map.of()); + } + + private static JsonMatchAssertionEvaluator evaluatorForPath( + String path, + String expectedContent, + List ignore, + Map suiteVars, + Map 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)); } @@ -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."); + } }