diff --git a/allure-grpc/build.gradle.kts b/allure-grpc/build.gradle.kts index 4bc8d3567..259fc7e3d 100644 --- a/allure-grpc/build.gradle.kts +++ b/allure-grpc/build.gradle.kts @@ -8,14 +8,16 @@ description = "Allure gRPC Integration" val agent: Configuration by configurations.creating -val grpcVersion = "1.57.2" -val protobufVersion = "4.27.3" +val grpcVersion = "1.75.0" +val protobufVersion = "4.32.1" +val jacksonVersion = "2.17.2" dependencies { agent("org.aspectj:aspectjweaver") api(project(":allure-attachments")) + compileOnly("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") compileOnly("com.google.protobuf:protobuf-java-util:$protobufVersion") - compileOnly("io.grpc:grpc-core:$grpcVersion") + compileOnly("io.grpc:grpc-api:$grpcVersion") testImplementation("com.google.protobuf:protobuf-java-util:$protobufVersion") testImplementation("com.google.protobuf:protobuf-java:$protobufVersion") testImplementation("io.grpc:grpc-core:$grpcVersion") diff --git a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java index 35fc4977e..6518f5341 100644 --- a/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java +++ b/allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java @@ -27,21 +27,23 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; import io.qameta.allure.attachment.AttachmentData; -import io.qameta.allure.attachment.AttachmentProcessor; -import io.qameta.allure.attachment.DefaultAttachmentProcessor; +import io.qameta.allure.attachment.AttachmentRenderer; import io.qameta.allure.attachment.FreemarkerAttachmentRenderer; +import io.qameta.allure.model.Attachment; import io.qameta.allure.model.Status; import io.qameta.allure.model.StepResult; -import io.qameta.allure.util.ResultsUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.UUID; - -import static java.util.Objects.requireNonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Allure interceptor logger for gRPC. @@ -49,158 +51,433 @@ * @author dtuchs (Dmitry Tuchs). */ @SuppressWarnings({ - "checkstyle:ClassFanOutComplexity", - "checkstyle:AnonInnerLength", - "checkstyle:JavaNCSS" + "checkstyle:ClassFanOutComplexity", + "checkstyle:AnonInnerLength", + "checkstyle:JavaNCSS" }) public class AllureGrpc implements ClientInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AllureGrpc.class); - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); - - private String requestTemplatePath = "grpc-request.ftl"; - private String responseTemplatePath = "grpc-response.ftl"; + private static final String UNKNOWN = "unknown"; + private static final String JSON_SUFFIX = " (json)"; + private static final JsonFormat.Printer GRPC_TO_JSON_PRINTER = JsonFormat.printer(); - private boolean markStepFailedOnNonZeroCode = true; - private boolean interceptResponseMetadata; - - public AllureGrpc setRequestTemplate(final String templatePath) { - this.requestTemplatePath = templatePath; - return this; - } + private final AllureLifecycle lifecycle; + private final boolean markStepFailedOnNonZeroCode; + private final boolean interceptResponseMetadata; + private final String requestTemplatePath; + private final String responseTemplatePath; - public AllureGrpc setResponseTemplate(final String templatePath) { - this.responseTemplatePath = templatePath; - return this; + public AllureGrpc() { + this(Allure.getLifecycle(), true, false, + "grpc-request.ftl", "grpc-response.ftl"); } - public AllureGrpc markStepFailedOnNonZeroCode(final boolean value) { - this.markStepFailedOnNonZeroCode = value; - return this; - } - - public AllureGrpc interceptResponseMetadata(final boolean value) { - this.interceptResponseMetadata = value; - return this; + public AllureGrpc( + final AllureLifecycle lifecycle, + final boolean markStepFailedOnNonZeroCode, + final boolean interceptResponseMetadata, + final String requestTemplatePath, + final String responseTemplatePath + ) { + this.lifecycle = lifecycle; + this.markStepFailedOnNonZeroCode = markStepFailedOnNonZeroCode; + this.interceptResponseMetadata = interceptResponseMetadata; + this.requestTemplatePath = requestTemplatePath; + this.responseTemplatePath = responseTemplatePath; } @Override - public ClientCall interceptCall(final MethodDescriptor method, - final CallOptions callOptions, - final Channel next) { - final AttachmentProcessor processor = new DefaultAttachmentProcessor(); + public ClientCall interceptCall( + final MethodDescriptor methodDescriptor, + final CallOptions callOptions, + final Channel nextChannel + ) { + final AllureLifecycle current = lifecycle; + final String parent = current.getCurrentTestCaseOrStep().orElse(null); + final String stepUuid = UUID.randomUUID().toString(); + final List clientMessages = new ArrayList<>(); + final List serverMessages = new ArrayList<>(); + final Map initialHeaders = new LinkedHashMap<>(); + final Map trailers = new LinkedHashMap<>(); - return new ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions.withoutWaitForReady())) { + final String stepName = buildStepName(nextChannel, methodDescriptor); + if (parent != null) { + current.startStep(parent, stepUuid, new StepResult().setName(stepName)); + } else { + current.startStep(stepUuid, new StepResult().setName(stepName)); + } - private String stepUuid; - private final List parsedResponses = new ArrayList<>(); - - @Override - public void sendMessage(final T message) { - stepUuid = UUID.randomUUID().toString(); - Allure.getLifecycle().startStep(stepUuid, (new StepResult()).setName( - "Send gRPC request to " - + next.authority() - + trimGrpcMethodName(method.getFullMethodName()) - )); - try { - final GrpcRequestAttachment rpcRequestAttach = GrpcRequestAttachment.Builder - .create("gRPC request", method.getFullMethodName()) - .setBody(JSON_PRINTER.print((MessageOrBuilder) message)) - .build(); - processor.addAttachment(rpcRequestAttach, new FreemarkerAttachmentRenderer(requestTemplatePath)); - super.sendMessage(message); - } catch (InvalidProtocolBufferException e) { - LOGGER.warn("Can`t parse gRPC request", e); - } catch (Throwable e) { - Allure.getLifecycle().updateStep(stepResult -> - stepResult.setStatus(ResultsUtils.getStatus(e).orElse(Status.BROKEN)) - .setStatusDetails(ResultsUtils.getStatusDetails(e).orElse(null)) - ); - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - } - } + final StepContext stepContext = new StepContext<>( + stepUuid, methodDescriptor, current, clientMessages, + serverMessages, initialHeaders, trailers + ); + return new ForwardingClientCall.SimpleForwardingClientCall( + nextChannel.newCall(methodDescriptor, callOptions) + ) { @Override - public void start(final Listener responseListener, final Metadata headers) { - final ClientCall.Listener listener = new ForwardingClientCallListener() { + public void start(final Listener responseListener, final Metadata requestHeaders) { + final Listener forwardingListener = new ForwardingClientCallListener() { @Override - protected Listener delegate() { + protected Listener delegate() { return responseListener; } @Override - public void onClose(final io.grpc.Status status, final Metadata trailers) { - GrpcResponseAttachment.Builder responseAttachmentBuilder = null; - - if (parsedResponses.size() == 1) { - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create("gRPC response") - .setBody(parsedResponses.iterator().next()); - } else if (parsedResponses.size() > 1) { - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create("gRPC response (collection of elements from Server stream)") - .setBody("[" + String.join(",\n", parsedResponses) + "]"); - } - if (!status.isOk()) { - String description = status.getDescription(); - if (description == null) { - description = "No description provided"; - } - responseAttachmentBuilder = GrpcResponseAttachment.Builder - .create(status.getCode().name()) - .setStatus(description); - } - - requireNonNull(responseAttachmentBuilder).setStatus(status.toString()); - if (interceptResponseMetadata) { - for (String key : headers.keys()) { - requireNonNull(responseAttachmentBuilder).setMetadata( - key, - headers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)) - ); - } - } - processor.addAttachment( - requireNonNull(responseAttachmentBuilder).build(), - new FreemarkerAttachmentRenderer(responseTemplatePath) - ); - - if (status.isOk() || !markStepFailedOnNonZeroCode) { - Allure.getLifecycle().updateStep(stepUuid, step -> step.setStatus(Status.PASSED)); - } else { - Allure.getLifecycle().updateStep(stepUuid, step -> step.setStatus(Status.FAILED)); - } - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - super.onClose(status, trailers); + public void onHeaders(final Metadata headers) { + handleHeaders(headers, stepContext.getInitialHeaders()); + super.onHeaders(headers); + } + + @Override + public void onMessage(final R message) { + handleServerMessage(message, stepContext.getServerMessages()); + super.onMessage(message); } @Override - public void onMessage(final A message) { - try { - parsedResponses.add(JSON_PRINTER.print((MessageOrBuilder) message)); - super.onMessage(message); - } catch (InvalidProtocolBufferException e) { - LOGGER.warn("Can`t parse gRPC response", e); - } catch (Throwable e) { - Allure.getLifecycle().updateStep(step -> - step.setStatus(ResultsUtils.getStatus(e).orElse(Status.BROKEN)) - .setStatusDetails(ResultsUtils.getStatusDetails(e).orElse(null)) - ); - Allure.getLifecycle().stopStep(stepUuid); - stepUuid = null; - } + public void onClose(final io.grpc.Status status, final Metadata responseTrailers) { + handleClose(status, responseTrailers, stepContext); + super.onClose(status, responseTrailers); } }; - super.start(listener, headers); + super.start(forwardingListener, requestHeaders); } - private String trimGrpcMethodName(final String source) { - return source.substring(source.lastIndexOf('/')); + @Override + public void sendMessage(final T message) { + handleClientMessage(message, stepContext.getClientMessages()); + super.sendMessage(message); } }; } + + private void addRawJsonAttachment( + final String stepUuid, + final String attachmentName, + final String jsonBody, + final AllureLifecycle lifecycle + ) { + if (jsonBody == null || jsonBody.isEmpty()) { + return; + } + final String source = UUID.randomUUID() + ".json"; + lifecycle.updateStep(stepUuid, step -> step.getAttachments().add( + new Attachment() + .setName(attachmentName) + .setSource(source) + .setType("application/json") + )); + lifecycle.writeAttachment( + source, + new ByteArrayInputStream(jsonBody.getBytes(StandardCharsets.UTF_8)) + ); + } + + private void handleClose( + final io.grpc.Status status, + final Metadata responseTrailers, + final StepContext stepContext + ) { + try { + if (interceptResponseMetadata && responseTrailers != null) { + copyAsciiResponseMetadata(responseTrailers, stepContext.getTrailers()); + } + attachRequestIfPresent( + stepContext.getStepUuid(), + stepContext.getMethodDescriptor(), + stepContext.getClientMessages(), + stepContext.getLifecycle() + ); + attachResponse( + stepContext.getStepUuid(), + stepContext.getServerMessages(), + status, + stepContext.getInitialHeaders(), + stepContext.getTrailers(), + stepContext.getLifecycle() + ); + stepContext.getLifecycle().updateStep( + stepContext.getStepUuid(), + step -> step.setStatus(convertStatus(status)) + ); + } catch (Throwable throwable) { + LOGGER.error("Failed to finalize Allure step for gRPC call", throwable); + stepContext.getLifecycle().updateStep( + stepContext.getStepUuid(), + step -> step.setStatus(Status.BROKEN) + ); + } finally { + stopStepSafely(stepContext.getLifecycle(), stepContext.getStepUuid()); + } + } + + private void handleHeaders(final Metadata headers, final Map destination) { + try { + if (interceptResponseMetadata && headers != null) { + copyAsciiResponseMetadata(headers, destination); + } + } catch (Throwable throwable) { + LOGGER.warn("Failed to capture response headers", throwable); + } + } + + private void handleClientMessage(final T message, final List destination) { + try { + destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message)); + } catch (InvalidProtocolBufferException e) { + LOGGER.error("Could not serialize gRPC request message to JSON", e); + } catch (Throwable throwable) { + LOGGER.error("Unexpected error while serializing gRPC request message", throwable); + } + } + + private void handleServerMessage(final R message, final List destination) { + try { + destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message)); + } catch (InvalidProtocolBufferException e) { + LOGGER.error("Could not serialize gRPC response message to JSON", e); + } catch (Throwable throwable) { + LOGGER.error("Unexpected error while serializing gRPC response message", throwable); + } + } + + private void attachRequestIfPresent( + final String stepUuid, + final MethodDescriptor methodDescriptor, + final List clientMessages, + final AllureLifecycle lifecycle + ) { + final String body = toJsonBody(clientMessages); + if (body == null) { + return; + } + final String name = clientMessages.size() > 1 + ? "gRPC request (collection of elements from Client stream)" + : "gRPC request"; + final GrpcRequestAttachment requestAttachment = GrpcRequestAttachment.Builder + .create(name, methodDescriptor.getFullMethodName()) + .setBody(body) + .build(); + + addRenderedAttachmentToStep( + stepUuid, + requestAttachment.getName(), + requestAttachment, + requestTemplatePath, + lifecycle + ); + addRawJsonAttachment(stepUuid, name + JSON_SUFFIX, body, lifecycle); + } + + private void attachResponse( + final String stepUuid, + final List serverMessages, + final io.grpc.Status status, + final Map initialHeaders, + final Map trailers, + final AllureLifecycle lifecycle + ) { + final String body = toJsonBody(serverMessages); + final String name = serverMessages.size() > 1 + ? "gRPC response (collection of elements from Server stream)" + : "gRPC response"; + + final Map metadata = new LinkedHashMap<>(); + if (interceptResponseMetadata) { + metadata.putAll(initialHeaders); + metadata.putAll(trailers); + } + + final GrpcResponseAttachment.Builder builder = GrpcResponseAttachment.Builder + .create(name) + .setStatus(status.toString()); + + if (body != null) { + builder.setBody(body); + } + if (!metadata.isEmpty()) { + builder.addMetadata(metadata); + } + + final GrpcResponseAttachment responseAttachment = builder.build(); + addRenderedAttachmentToStep( + stepUuid, + responseAttachment.getName(), + responseAttachment, + responseTemplatePath, + lifecycle + ); + if (body != null) { + addRawJsonAttachment(stepUuid, name + JSON_SUFFIX, body, lifecycle); + } + } + + private void stopStepSafely(final AllureLifecycle lifecycle, final String stepUuid) { + try { + lifecycle.stopStep(stepUuid); + } catch (Throwable throwable) { + LOGGER.warn("Failed to stop Allure step {}", stepUuid, throwable); + } + } + + private Status convertStatus(final io.grpc.Status grpcStatus) { + if (grpcStatus.isOk() || !markStepFailedOnNonZeroCode) { + return Status.PASSED; + } + return Status.FAILED; + } + + private static String buildStepName( + final Channel channel, + final MethodDescriptor methodDescriptor + ) { + final String authority = channel != null ? channel.authority() : null; + final String safeAuthority = authority != null ? authority : UNKNOWN; + final String type = toSnakeCase(methodDescriptor.getType()); + return "Send " + type + " gRPC request to " + + safeAuthority + "/" + methodDescriptor.getFullMethodName(); + } + + private static String toSnakeCase(final MethodDescriptor.MethodType methodType) { + if (methodType == null) { + return UNKNOWN; + } + return methodType.name().toLowerCase(Locale.ROOT); + } + + private void addRenderedAttachmentToStep( + final String stepUuid, + final String attachmentName, + final AttachmentData data, + final String templatePath, + final AllureLifecycle lifecycle + ) { + final AttachmentRenderer renderer = + new FreemarkerAttachmentRenderer(templatePath); + final io.qameta.allure.attachment.AttachmentContent content; + try { + content = renderer.render(data); + } catch (Throwable throwable) { + LOGGER.warn( + "Could not render attachment '{}' using template '{}'", + attachmentName, templatePath, throwable + ); + return; + } + if (content == null || content.getContent() == null) { + LOGGER.warn("Rendered attachment '{}' is empty; skipping", attachmentName); + return; + } + String fileExtension = content.getFileExtension(); + if (fileExtension == null || fileExtension.isEmpty()) { + fileExtension = ".html"; + } + final String source = UUID.randomUUID() + fileExtension; + lifecycle.updateStep( + stepUuid, + step -> step.getAttachments().add( + new Attachment() + .setName(attachmentName) + .setSource(source) + .setType( + content.getContentType() != null + ? content.getContentType() + : "text/html" + ) + ) + ); + lifecycle.writeAttachment( + source, + new ByteArrayInputStream(content.getContent().getBytes(StandardCharsets.UTF_8)) + ); + } + + private static String toJsonBody(final List items) { + if (items == null || items.isEmpty()) { + return null; + } + if (items.size() == 1) { + return items.get(0); + } + final String joined = String.join(",\n", items); + return "[" + joined + "]"; + } + + private static void copyAsciiResponseMetadata( + final Metadata source, + final Map target + ) { + for (String key : source.keys()) { + if (key == null) { + continue; + } + if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + continue; + } + final Metadata.Key keyAscii = + Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + final String value = source.get(keyAscii); + if (value != null) { + target.put(key, value); + } + } + } + + private static final class StepContext { + private final String stepUuid; + private final MethodDescriptor methodDescriptor; + private final AllureLifecycle lifecycle; + private final List clientMessages; + private final List serverMessages; + private final Map initialHeaders; + private final Map trailers; + + StepContext( + final String stepUuid, + final MethodDescriptor methodDescriptor, + final AllureLifecycle lifecycle, + final List clientMessages, + final List serverMessages, + final Map initialHeaders, + final Map trailers + ) { + this.stepUuid = stepUuid; + this.methodDescriptor = methodDescriptor; + this.lifecycle = lifecycle; + this.clientMessages = clientMessages; + this.serverMessages = serverMessages; + this.initialHeaders = initialHeaders; + this.trailers = trailers; + } + + String getStepUuid() { + return stepUuid; + } + + MethodDescriptor getMethodDescriptor() { + return methodDescriptor; + } + + AllureLifecycle getLifecycle() { + return lifecycle; + } + + List getClientMessages() { + return clientMessages; + } + + List getServerMessages() { + return serverMessages; + } + + Map getInitialHeaders() { + return initialHeaders; + } + + Map getTrailers() { + return trailers; + } + } } diff --git a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java index 6c83b9f03..e316fc75a 100644 --- a/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java +++ b/allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java @@ -15,12 +15,17 @@ */ package io.qameta.allure.grpc; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.qameta.allure.Allure; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; import io.qameta.allure.test.AllureResults; import org.grpcmock.GrpcMock; import org.grpcmock.junit5.GrpcMockExtension; @@ -29,135 +34,348 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Optional; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.grpcmock.GrpcMock.bidiStreamingMethod; +import static org.grpcmock.GrpcMock.clientStreamingMethod; import static org.grpcmock.GrpcMock.serverStreamingMethod; import static org.grpcmock.GrpcMock.unaryMethod; -/** - * @author dtuchs (Dmitry Tuchs). - */ @ExtendWith(GrpcMockExtension.class) class AllureGrpcTest { private static final String RESPONSE_MESSAGE = "Hello world!"; + private static final ObjectMapper JSON = new ObjectMapper(); - private ManagedChannel channel; - private TestServiceGrpc.TestServiceBlockingStub blockingStub; + private ManagedChannel managedChannel; @BeforeEach - void configureMock() { - channel = ManagedChannelBuilder.forAddress("localhost", GrpcMock.getGlobalPort()) - .usePlaintext() - .build(); - blockingStub = TestServiceGrpc.newBlockingStub(channel) - .withInterceptors(new AllureGrpc()); + void configureMockServer() { + managedChannel = ManagedChannelBuilder + .forAddress("localhost", GrpcMock.getGlobalPort()) + .usePlaintext() + .directExecutor() + .build(); GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) - .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + GrpcMock.stubFor(serverStreamingMethod(TestServiceGrpc.getCalculateServerStreamMethod()) - .willReturn(asList( - Response.newBuilder().setMessage(RESPONSE_MESSAGE).build(), - Response.newBuilder().setMessage(RESPONSE_MESSAGE).build() - ))); + .willReturn(asList( + Response.newBuilder().setMessage(RESPONSE_MESSAGE).build(), + Response.newBuilder().setMessage(RESPONSE_MESSAGE).build() + ))); + + GrpcMock.stubFor(clientStreamingMethod(TestServiceGrpc.getCalculateClientStreamMethod()) + .willReturn(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build())); + + GrpcMock.stubFor(bidiStreamingMethod(TestServiceGrpc.getCalculateBidiStreamMethod()) + .willProxyTo(responseObserver -> new StreamObserver() { + @Override + public void onNext(Request request) { + responseObserver.onNext(Response.newBuilder().setMessage(RESPONSE_MESSAGE).build()); + } + @Override + public void onError(Throwable throwable) { + } + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + })); } @AfterEach void shutdownChannel() { - Optional.ofNullable(channel).ifPresent(ManagedChannel::shutdownNow); + Optional.ofNullable(managedChannel).ifPresent(ManagedChannel::shutdown); } @Test void shouldCreateRequestAttachment() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); + + Status errorStatus = Status.NOT_FOUND; + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()).willReturn(errorStatus)); - final AllureResults results = execute(request); + AllureResults allureResults = executeUnaryExpectingException(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC request"); + assertThat(allureResults.getTestResults().get(0).getSteps().get(0).getStatus()) + .isEqualTo(io.qameta.allure.model.Status.FAILED); + + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC request", "gRPC response"); } @Test void shouldCreateResponseAttachment() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); - final AllureResults results = execute(request); + AllureResults allureResults = executeUnary(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC response"); + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response"); } @Test void shouldCreateResponseAttachmentForServerStreamingResponse() { - final Request request = Request.newBuilder() - .setTopic("1") - .build(); + Request request = Request.newBuilder() + .setTopic("1") + .build(); - final AllureResults results = executeStreaming(request); + AllureResults allureResults = executeServerStreaming(request); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains("gRPC response (collection of elements from Server stream)"); + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response (collection of elements from Server stream)"); } @Test void shouldCreateResponseAttachmentOnStatusException() { - final Status status = Status.NOT_FOUND; + Status notFoundStatus = Status.NOT_FOUND; + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()).willReturn(notFoundStatus)); + + Request request = Request.newBuilder() + .setTopic("2") + .build(); + + AllureResults allureResults = executeUnaryExpectingException(request); + + assertThat(allureResults.getTestResults().get(0).getSteps().get(0).getStatus()) + .isEqualTo(io.qameta.allure.model.Status.FAILED); + + assertThat(allureResults.getTestResults().get(0).getSteps()) + .flatExtracting(StepResult::getAttachments) + .extracting(Attachment::getName) + .contains("gRPC response"); + } + + @Test + void shouldCreateAttachmentsForClientStreamingWithAsynchronousStub() { + Request firstClientRequest = Request.newBuilder().setTopic("A").build(); + Request secondClientRequest = Request.newBuilder().setTopic("B").build(); + + runWithinTestContext(() -> { + TestServiceGrpc.TestServiceStub asynchronousStub = + TestServiceGrpc.newStub(managedChannel).withInterceptors(new AllureGrpc()); + + final List receivedResponses = new ArrayList(); + + Allure.step("async-root-client-stream", () -> { + StreamObserver responseObserver = new StreamObserver() { + @Override + public void onNext(Response value) { + receivedResponses.add(value); + } + @Override + public void onError(Throwable throwable) { + } + @Override + public void onCompleted() { + } + }; + + StreamObserver requestObserver = asynchronousStub.calculateClientStream(responseObserver); + requestObserver.onNext(firstClientRequest); + requestObserver.onNext(secondClientRequest); + requestObserver.onCompleted(); + }); + + assertThat(receivedResponses).hasSize(1); + assertThat(receivedResponses.get(0).getMessage()).isEqualTo(RESPONSE_MESSAGE); + }); + } + + @Test + void shouldCreateAttachmentsForBidirectionalStreamingWithAsynchronousStub() { + Request firstBidirectionalRequest = Request.newBuilder().setTopic("C").build(); + Request secondBidirectionalRequest = Request.newBuilder().setTopic("D").build(); + + runWithinTestContext(() -> { + TestServiceGrpc.TestServiceStub asynchronousStub = + TestServiceGrpc.newStub(managedChannel).withInterceptors(new AllureGrpc()); + + List receivedResponses = new ArrayList<>(); + + Allure.step("async-root-bidi-stream", () -> { + StreamObserver responseObserver = new StreamObserver() { + @Override public void onNext(Response value) { receivedResponses.add(value); } + @Override public void onError(Throwable throwable) { } + @Override public void onCompleted() { } + }; + + StreamObserver requestObserver = asynchronousStub.calculateBidiStream(responseObserver); + requestObserver.onNext(firstBidirectionalRequest); + requestObserver.onNext(secondBidirectionalRequest); + requestObserver.onCompleted(); + }); + + assertThat(receivedResponses).hasSize(2); + assertThat(receivedResponses.get(0).getMessage()).isEqualTo(RESPONSE_MESSAGE); + assertThat(receivedResponses.get(1).getMessage()).isEqualTo(RESPONSE_MESSAGE); + }); + } + + @Test + void unaryRequestBodyIsCapturedAsJsonObject() throws Exception { + GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) + .willReturn(Response.newBuilder().setMessage("ok").build())); + + Request request = Request.newBuilder().setTopic("topic-1").build(); + + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("ok"); + }); + + String jsonPayload = readJsonAttachmentByName(allureResults, "gRPC request (json)"); + JsonNode actualJsonNode = JSON.readTree(jsonPayload); + JsonNode expectedJsonNode = JSON.createObjectNode().put("topic", "topic-1"); + + assertThat(actualJsonNode).isEqualTo(expectedJsonNode); + } + + @Test + void unaryResponseBodyIsCapturedAsJsonObject() throws Exception { GrpcMock.stubFor(unaryMethod(TestServiceGrpc.getCalculateMethod()) - .willReturn(status)); + .willReturn(Response.newBuilder().setMessage("hello-world").build())); - final Request request = Request.newBuilder() - .setTopic("2") - .build(); + Request request = Request.newBuilder().setTopic("x").build(); - final AllureResults results = executeException(request); + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("hello-world"); + }); + + String jsonPayload = readJsonAttachmentByName(allureResults, "gRPC response (json)"); + JsonNode actualJsonNode = JSON.readTree(jsonPayload); + JsonNode expectedJsonNode = JSON.createObjectNode().put("message", "hello-world"); - assertThat(results.getTestResults().get(0).getSteps()) - .flatExtracting(StepResult::getAttachments) - .extracting(Attachment::getName) - .contains(status.getCode().name()); + assertThat(actualJsonNode).isEqualTo(expectedJsonNode); } - protected final AllureResults execute(final Request request) { + @Test + void serverStreamingResponseBodyIsJsonArrayInOrder() throws Exception { + GrpcMock.stubFor(serverStreamingMethod(TestServiceGrpc.getCalculateServerStreamMethod()) + .willReturn(asList( + Response.newBuilder().setMessage("first").build(), + Response.newBuilder().setMessage("second").build() + ))); + + Request request = Request.newBuilder().setTopic("stream-topic").build(); + + AllureResults allureResults = runWithinTestContext(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Iterator responseIterator = stub.calculateServerStream(request); + assertThat(responseIterator.hasNext()).isTrue(); + assertThat(responseIterator.next().getMessage()).isEqualTo("first"); + assertThat(responseIterator.hasNext()).isTrue(); + assertThat(responseIterator.next().getMessage()).isEqualTo("second"); + assertThat(responseIterator.hasNext()).isFalse(); + }); + + String jsonPayload = readJsonAttachmentByName( + allureResults, "gRPC response (collection of elements from Server stream) (json)" + ); + JsonNode actualJsonArray = JSON.readTree(jsonPayload); + + assertThat(actualJsonArray.isArray()).isTrue(); + assertThat(actualJsonArray.size()).isEqualTo(2); + assertThat(actualJsonArray.get(0)).isEqualTo(JSON.createObjectNode().put("message", "first")); + assertThat(actualJsonArray.get(1)).isEqualTo(JSON.createObjectNode().put("message", "second")); + } + private static String readJsonAttachmentByName(AllureResults allureResults, String jsonAttachmentName) { + TestResult test = allureResults.getTestResults().get(0); + + Attachment matchedAttachment = flattenSteps(test.getSteps()).stream() + .flatMap(step -> step.getAttachments().stream()) + .filter(attachment -> jsonAttachmentName.equals(attachment.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Attachment not found: " + jsonAttachmentName)); + + String attachmentSourceKey = matchedAttachment.getSource(); + Map attachmentsContent = allureResults.getAttachments(); + byte[] rawAttachmentContent = attachmentsContent.get(attachmentSourceKey); + if (rawAttachmentContent == null) { + throw new IllegalStateException("Attachment content not found by source: " + attachmentSourceKey); + } + return new String(rawAttachmentContent, StandardCharsets.UTF_8); + } + + protected final AllureResults executeUnary(Request request) { return runWithinTestContext(() -> { try { - final Response response = blockingStub.calculate(request); + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); assertThat(response.getMessage()).isEqualTo(RESPONSE_MESSAGE); - } catch (Exception e) { - throw new RuntimeException("Could not execute request " + request, e); + } catch (Exception exception) { + throw new RuntimeException("Could not execute request " + request, exception); } }); } - protected final AllureResults executeStreaming(final Request request) { + + protected final AllureResults executeServerStreaming(Request request) { return runWithinTestContext(() -> { try { - Iterator responseIterator = blockingStub.calculateServerStream(request); + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Iterator responseIterator = stub.calculateServerStream(request); + int responseCount = 0; while (responseIterator.hasNext()) { assertThat(responseIterator.next().getMessage()).isEqualTo(RESPONSE_MESSAGE); + responseCount++; } - } catch (Exception e) { - throw new RuntimeException("Could not execute request " + request, e); + assertThat(responseCount).isEqualTo(2); + } catch (Exception exception) { + throw new RuntimeException("Could not execute request " + request, exception); } }); } - protected final AllureResults executeException(final Request request) { - return runWithinTestContext(() -> { - assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> blockingStub.calculate(request)); - }); + protected final AllureResults executeUnaryExpectingException(Request request) { + return runWithinTestContext(() -> + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> { + TestServiceGrpc.TestServiceBlockingStub stub = + TestServiceGrpc.newBlockingStub(managedChannel).withInterceptors(new AllureGrpc()); + Response response = stub.calculate(request); + assertThat(response.getMessage()).isEqualTo("ok"); + }) + ); + } + + private static List flattenSteps(List rootSteps) { + List allSteps = new ArrayList<>(); + if (rootSteps == null) { + return allSteps; + } + for (StepResult step : rootSteps) { + allSteps.add(step); + allSteps.addAll(flattenSteps(step.getSteps())); + } + return allSteps; } } diff --git a/allure-grpc/src/test/proto/api.proto b/allure-grpc/src/test/proto/api.proto index 552e76f6c..378bb0872 100644 --- a/allure-grpc/src/test/proto/api.proto +++ b/allure-grpc/src/test/proto/api.proto @@ -6,6 +6,8 @@ option java_package = "io.qameta.allure.grpc"; service TestService { rpc Calculate (Request) returns (Response); rpc CalculateServerStream (Request) returns (stream Response); + rpc CalculateClientStream (stream Request) returns (Response); + rpc CalculateBidiStream (stream Request) returns (stream Response); } message Request {