From f88588b69b76f2b6c21a1a8e1cb8bd578fcd1e89 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 4 Jun 2026 20:44:29 +0300 Subject: [PATCH 1/2] Issue-1490 draft: Implement custom logging filter for RestAssured --- java-reporter-core/dependency-reduced-pom.xml | 10 +- java-reporter-core/pom.xml | 6 + .../io/testomat/core/facade/Testomatio.java | 9 +- .../core/restassured/TestomatHttpFilter.java | 186 ++++++++++++++++++ .../io/testomat/core/step/StepAspect.java | 26 ++- 5 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java diff --git a/java-reporter-core/dependency-reduced-pom.xml b/java-reporter-core/dependency-reduced-pom.xml index c04e42d..d848085 100644 --- a/java-reporter-core/dependency-reduced-pom.xml +++ b/java-reporter-core/dependency-reduced-pom.xml @@ -1,10 +1,10 @@ 4.0.0 - coretest + io.testomat java-reporter-core Testomat.io Reporter Core - coretest + 0.13.1 Core functionality for Testomat.io Java test reporting library https://github.com/testomatio/java-reporter @@ -271,6 +271,12 @@ + + io.rest-assured + rest-assured + 5.5.7 + provided + diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index 94f94df..8622435 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -104,6 +104,12 @@ 5.8.0 test + + io.rest-assured + rest-assured + 5.5.7 + provided + diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java index c970821..7f3a368 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java @@ -81,8 +81,9 @@ public static void step(String stepName, Runnable action) { step.setStatus(StepStatus.failed); step.setLog(getStackTrace(t)); step.setError( - Optional.ofNullable(t.getMessage()) - .orElse(t.getClass().getSimpleName()) + t.getMessage() == null || t.getMessage().isBlank() + ? t.getClass().getSimpleName() + : t.getClass().getSimpleName() + ": " + t.getMessage() ); throwUnchecked(t); } finally { @@ -92,6 +93,10 @@ public static void step(String stepName, Runnable action) { } } + static void step(String stepName, String passedLog, Runnable action) { + + } + public static void meta(String key, String value) { MetaStorage.TEMP_META_STORAGE.get().put(key, value); } diff --git a/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java b/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java new file mode 100644 index 0000000..5fcef9a --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java @@ -0,0 +1,186 @@ +package io.testomat.core.restassured; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; +import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.TestStep; +import java.net.URI; + +public class TestomatHttpFilter implements Filter { + + private final boolean requestBody; + private final boolean responseBody; + private final boolean headers; + private final boolean onlyFailures; + + /** + * Creates filter with default settings. + */ + public static TestomatHttpFilter create() { + return new Builder().build(); + } + + /** + * Creates builder for custom configuration. + */ + public static Builder custom() { + return new Builder(); + } + + private TestomatHttpFilter(Builder builder) { + this.requestBody = builder.requestBody; + this.responseBody = builder.responseBody; + this.headers = builder.headers; + this.onlyFailures = builder.onlyFailures; + } + + @Override + public Response filter( + FilterableRequestSpecification request, + FilterableResponseSpecification responseSpec, + FilterContext ctx) { + + long startTime = System.currentTimeMillis(); + + Response response = ctx.next(request, responseSpec); + + long duration = System.currentTimeMillis() - startTime; + + if (onlyFailures && response.statusCode() < 400) { + return response; + } + + TestStep testStep = new TestStep(); + testStep.setCategory("user"); + StepLifecycle.start(testStep); + + String summary = buildSummary( + request, + response, + duration + ); + + stepWrapper("HTTP method: " + request.getMethod(), ""); + stepWrapper("URL: " + request.getURI(), ""); + stepWrapper("Status code: " + response.statusCode(), ""); + stepWrapper("Duration: " + duration + " ms", ""); + stepWrapper("Request headers: ", request.getHeaders().toString()); + stepWrapper("Response headers: ", response.getHeaders().toString()); + stepWrapper("Request body: ", response.getBody().asPrettyString()); + stepWrapper("Response body: ", response.getBody().asPrettyString()); + + testStep.setStepTitle(summary); + StepLifecycle.finish(); + + return response; + } + + private String buildSummary(FilterableRequestSpecification request, Response response, long duration) { + String path = URI.create(request.getURI()).getPath(); + + return String.format("%s %s → %d (%d ms)", + request.getMethod(), + path, + response.statusCode(), + duration + ); + } + + private String buildDetails( + FilterableRequestSpecification request, + Response response, + long duration) { + + StringBuilder sb = new StringBuilder(); + + sb.append("HTTP method: ") + .append(request.getMethod()) + .append("\n"); + + sb.append("URL: ") + .append(request.getURI()) + .append("\n"); + + sb.append("Status code: ") + .append(response.statusCode()) + .append("\n"); + + sb.append("Duration: ") + .append(duration) + .append(" ms\n\n"); + + if (headers) { + sb.append("Request headers:\n") + .append(request.getHeaders()) + .append("\n\n"); + + sb.append("Response headers:\n") + .append(response.getHeaders()) + .append("\n\n"); + } + + if (requestBody) { + sb.append("Request body:\n") + .append(request.getBody() == null + ? "" + : request.getBody().toString()) + .append("\n\n"); + } + + if (responseBody) { + sb.append("Response body:\n") + .append(response.getBody().asPrettyString()) + .append("\n"); + } + + return sb.toString(); + } + + public static final class Builder { + + private boolean requestBody = true; + private boolean responseBody = true; + private boolean headers = true; + private boolean onlyFailures = false; + + private Builder() { + } + + public Builder requestBody(boolean requestBody) { + this.requestBody = requestBody; + return this; + } + + public Builder responseBody(boolean responseBody) { + this.responseBody = responseBody; + return this; + } + + public Builder headers(boolean headers) { + this.headers = headers; + return this; + } + + public Builder onlyFailures(boolean onlyFailures) { + this.onlyFailures = onlyFailures; + return this; + } + + public TestomatHttpFilter build() { + return new TestomatHttpFilter(this); + } + } + + private void stepWrapper(String summary, String details) { + TestStep testStep = new TestStep(); + testStep.setCategory("user"); + StepLifecycle.start(testStep); + + testStep.setStepTitle(summary); + testStep.setLog(details); + StepLifecycle.finish(); + } +} \ No newline at end of file diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java index 1332944..8c24e27 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java @@ -3,6 +3,8 @@ import static io.testomat.core.facade.Testomatio.stepArtifact; import io.testomat.core.annotation.Step; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Method; import java.util.Arrays; import org.aspectj.lang.JoinPoint; @@ -95,10 +97,10 @@ public void afterSuccess(JoinPoint joinPoint) { * and completes the step lifecycle. * * @param joinPoint intercepted method invocation - * @param e thrown exception + * @param t thrown exception */ - @AfterThrowing(pointcut = "stepAnnotation()", throwing = "e") - public void afterFailure(JoinPoint joinPoint, Throwable e) { + @AfterThrowing(pointcut = "stepAnnotation()", throwing = "t") + public void afterFailure(JoinPoint joinPoint, Throwable t) { Step step = resolveStepAnnotation(joinPoint); TestStep testStep = StepLifecycle.current(); @@ -113,20 +115,24 @@ public void afterFailure(JoinPoint joinPoint, Throwable e) { testStep.setStatus(StepStatus.failed); testStep.setDuration(duration); - testStep.setError(e.getMessage()); - testStep.setLog(Arrays.toString(e.getStackTrace())); + testStep.setError( + t.getMessage() == null || t.getMessage().isBlank() + ? t.getClass().getSimpleName() + : t.getClass().getSimpleName() + ": " + t.getMessage() + ); + testStep.setLog(getStackTrace(t)); if (artifacts != null) { stepArtifact(artifacts); } - log.debug("Step '{}' failed in {} ms", stepName, duration, e); + log.debug("Step '{}' failed in {} ms", stepName, duration, t); StepLifecycle.finish(); } private long calculateDuration(String stepId) { - return System.currentTimeMillis() - StepTimer.stop(stepId); + return StepTimer.stop(stepId); } private Step resolveStepAnnotation(JoinPoint joinPoint) { @@ -215,4 +221,10 @@ private String substituteParameters(String stepName, JoinPoint joinPoint) { private String format(Object value) { return value == null ? "null" : value.toString(); } + + private static String getStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } } \ No newline at end of file From 5f7ce5e725d65961e75a47b79618a7bc9ad5306c Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 9 Jul 2026 16:39:45 +0300 Subject: [PATCH 2/2] Issue-1490. feat: add RestAssured HTTP reporting filter and improve null handling --- README.md | 48 ++++++++ java-reporter-core/pom.xml | 2 +- .../core/constants/CommonConstants.java | 2 +- .../methods/artifact/client/AwsService.java | 7 +- .../core/restassured/TestomatHttpFilter.java | 106 +++++++----------- java-reporter-cucumber/pom.xml | 4 +- java-reporter-junit/pom.xml | 4 +- java-reporter-karate/pom.xml | 4 +- java-reporter-testng/pom.xml | 4 +- testomat-allure-adapter/pom.xml | 4 +- 10 files changed, 109 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index f135d0e..fd12acd 100644 --- a/README.md +++ b/README.md @@ -732,6 +732,54 @@ This provides complete transparency into test flow and helps debug failures quic --- +## HTTP Request Reporting + +The Testomat HTTP filter integrates with RestAssured and automatically reports HTTP requests as test steps. + +Each request is displayed as a parent step with detailed substeps containing: + +- HTTP method +- URL +- Status code +- Request duration +- Request headers +- Response headers +- Request body +- Response body + +### Basic usage + +```java +given() + .filter(TestomatHttpFilter.create()) + .when() + .get("/users"); +``` + +### Custom configuration + +```java +given() + .filter( + TestomatHttpFilter.builder() + .headers(false) + .requestBody(false) + .onlyFailures(true) + .build() + ); +``` + +### Available options + +| Option | Description | Default | +| -------------- | ------------------------------------------------- | ------- | +| `headers` | Include request and response headers | `true` | +| `requestBody` | Include request body | `true` | +| `responseBody` | Include response body | `true` | +| `onlyFailures` | Report only requests with HTTP status code >= 400 | `false` | + +--- + ## Test Filtering by ID **JUnit & TestNG only** diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index e145c85..6205781 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 jar Testomat.io Reporter Core diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java index 95b07d1..912ba3c 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java @@ -1,7 +1,7 @@ package io.testomat.core.constants; public class CommonConstants { - public static final String REPORTER_VERSION = "0.15.0"; + public static final String REPORTER_VERSION = "0.16.0"; public static final String TESTS_STRING = "tests"; public static final String API_KEY_STRING = "api_key"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java index 9907a8c..75bba1b 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/AwsService.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -74,7 +75,11 @@ public AwsService(ArtifactKeyGenerator keyGenerator, AwsClient awsClient, public void uploadAllArtifactsForTest(String testName, String rid, String testId) { List artifactDirectories = TempArtifactDirectoriesStorage.DIRECTORIES.get(); - Map stepArtifactDirectories = TempArtifactDirectoriesStorage.STEP_DATA.get(Thread.currentThread().getId()); + Map stepArtifactDirectories = + TempArtifactDirectoriesStorage.STEP_DATA.getOrDefault( + Thread.currentThread().getId(), + Collections.emptyMap()) + ; if (artifactDirectories.isEmpty() && stepArtifactDirectories.isEmpty()) { log.debug("Artifact list is empty for test: {}", testName); diff --git a/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java b/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java index 5fcef9a..88b3cdd 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java +++ b/java-reporter-core/src/main/java/io/testomat/core/restassured/TestomatHttpFilter.java @@ -5,11 +5,20 @@ import io.restassured.response.Response; import io.restassured.specification.FilterableRequestSpecification; import io.restassured.specification.FilterableResponseSpecification; +import io.testomat.core.facade.Testomatio; import io.testomat.core.step.StepLifecycle; import io.testomat.core.step.TestStep; +import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TestomatHttpFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger(TestomatHttpFilter.class); private final boolean requestBody; private final boolean responseBody; @@ -26,7 +35,7 @@ public static TestomatHttpFilter create() { /** * Creates builder for custom configuration. */ - public static Builder custom() { + public static Builder builder() { return new Builder(); } @@ -63,14 +72,24 @@ public Response filter( duration ); - stepWrapper("HTTP method: " + request.getMethod(), ""); - stepWrapper("URL: " + request.getURI(), ""); - stepWrapper("Status code: " + response.statusCode(), ""); - stepWrapper("Duration: " + duration + " ms", ""); - stepWrapper("Request headers: ", request.getHeaders().toString()); - stepWrapper("Response headers: ", response.getHeaders().toString()); - stepWrapper("Request body: ", response.getBody().asPrettyString()); - stepWrapper("Response body: ", response.getBody().asPrettyString()); + reportHttpData("Method: " + request.getMethod(), null); + reportHttpData("URL: " + request.getURI(), null); + reportHttpData("Status: " + response.statusCode(), null); + reportHttpData("Duration: " + duration + " ms", null); + + if (headers) { + reportHttpData("Request headers", request.getHeaders().toString()); + reportHttpData("Response headers", response.getHeaders().toString()); + } + + if (requestBody) { + reportHttpData("Request body", + request.getBody() == null ? "" : request.getBody().toString()); + } + + if (responseBody) { + reportHttpData("Response body", response.getBody().asPrettyString()); + } testStep.setStepTitle(summary); StepLifecycle.finish(); @@ -89,56 +108,6 @@ private String buildSummary(FilterableRequestSpecification request, Response res ); } - private String buildDetails( - FilterableRequestSpecification request, - Response response, - long duration) { - - StringBuilder sb = new StringBuilder(); - - sb.append("HTTP method: ") - .append(request.getMethod()) - .append("\n"); - - sb.append("URL: ") - .append(request.getURI()) - .append("\n"); - - sb.append("Status code: ") - .append(response.statusCode()) - .append("\n"); - - sb.append("Duration: ") - .append(duration) - .append(" ms\n\n"); - - if (headers) { - sb.append("Request headers:\n") - .append(request.getHeaders()) - .append("\n\n"); - - sb.append("Response headers:\n") - .append(response.getHeaders()) - .append("\n\n"); - } - - if (requestBody) { - sb.append("Request body:\n") - .append(request.getBody() == null - ? "" - : request.getBody().toString()) - .append("\n\n"); - } - - if (responseBody) { - sb.append("Response body:\n") - .append(response.getBody().asPrettyString()) - .append("\n"); - } - - return sb.toString(); - } - public static final class Builder { private boolean requestBody = true; @@ -174,13 +143,24 @@ public TestomatHttpFilter build() { } } - private void stepWrapper(String summary, String details) { + private void reportHttpData(String summary, String details) { TestStep testStep = new TestStep(); testStep.setCategory("user"); StepLifecycle.start(testStep); - testStep.setStepTitle(summary); - testStep.setLog(details); - StepLifecycle.finish(); + + try { + if (details != null && !details.isBlank()) { + Path dir = Paths.get("target", "testomat", "http"); + Files.createDirectories(dir); + Path json = Files.createTempFile(dir, "http-", ".json"); + Files.writeString(json, details, StandardCharsets.UTF_8); + Testomatio.stepArtifact(json.toString()); + } + } catch (IOException e) { + log.error("Failed to create HTTP attachment", e); + } finally { + StepLifecycle.finish(); + } } } \ No newline at end of file diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index 024e447..8102bcb 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.8.0 + 0.8.1 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 org.slf4j diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index 85ffad2..fa8b659 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.9.0 + 0.9.1 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 org.slf4j diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index 3d145ae..07d8d84 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.3.0 + 0.3.1 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 io.karatelabs diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index 9fb9d6d..e5b9055 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.8.0 + 0.8.1 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 org.slf4j diff --git a/testomat-allure-adapter/pom.xml b/testomat-allure-adapter/pom.xml index 036aca9..bf8672d 100644 --- a/testomat-allure-adapter/pom.xml +++ b/testomat-allure-adapter/pom.xml @@ -6,7 +6,7 @@ io.testomat testomat-allure-adapter - 0.1.1 + 0.1.2 jar Testomat.io Testomat Allure adapter @@ -67,7 +67,7 @@ io.testomat java-reporter-core - 0.15.0 + 0.16.0 io.qameta.allure