Skip to content
Open
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
9 changes: 6 additions & 3 deletions allure-grpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Use this module when your tests call gRPC services and you want method calls, me

- Allure Java 3.x requires Java 17 or newer.
- This module targets gRPC Java.
- The current build validates against gRPC Java 1.81.0 and Protobuf Java 4.35.0.
- The current build validates against gRPC Java 1.82.0 and Protobuf Java 4.35.1.

## Installation

Expand Down Expand Up @@ -42,19 +42,22 @@ ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.build();
```

For advanced capture policy, use the constructor that accepts an HTTP exchange builder customizer:
Request metadata capture is disabled by default. To enable request and response metadata capture
and apply redaction rules, use the advanced constructor:

```java
ClientInterceptor allure = new AllureGrpc(
Allure.getLifecycle(),
true,
true,
true,
exchange -> exchange.redactHeader("authorization")
);
```

## Report Output

- gRPC method calls as Allure steps.
- Request and response messages, metadata, status, and timing.
- Request and response messages, status, and timing.
- Optional request and response metadata when enabled through constructor flags.
- Stream metadata for unary and streaming calls where available.
93 changes: 81 additions & 12 deletions allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ public class AllureGrpc implements ClientInterceptor {

private final AllureLifecycle lifecycle;
private final boolean markStepFailedOnNonZeroCode;
private final boolean interceptRequestMetadata;
private final boolean interceptResponseMetadata;
private final Consumer<HttpExchange.Builder> exchangeCustomizer;

/**
* Creates an Allure grpc with default configuration.
*/
public AllureGrpc() {
this(Allure.getLifecycle(), true, false);
this(Allure.getLifecycle(), true, false, false);
}

/**
Expand All @@ -104,25 +105,72 @@ public AllureGrpc(
final AllureLifecycle lifecycle,
final boolean markStepFailedOnNonZeroCode,
final boolean interceptResponseMetadata) {
this(lifecycle, markStepFailedOnNonZeroCode, interceptResponseMetadata, builder -> {
});
this(lifecycle, markStepFailedOnNonZeroCode, false, interceptResponseMetadata);
}

/**
* Creates an Allure grpc with the supplied values.
*
* @param lifecycle the Allure lifecycle to use
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
* @param interceptRequestMetadata the intercept request metadata
* @param interceptResponseMetadata the intercept response metadata
*/
public AllureGrpc(
final AllureLifecycle lifecycle,
final boolean markStepFailedOnNonZeroCode,
final boolean interceptRequestMetadata,
final boolean interceptResponseMetadata) {
this(
lifecycle,
markStepFailedOnNonZeroCode,
interceptRequestMetadata,
interceptResponseMetadata,
builder -> {
}
);
}

/**
* Creates an Allure grpc with the supplied values.
*
* @param lifecycle the Allure lifecycle to use
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
* @param interceptResponseMetadata the intercept response metadata
* @param exchangeCustomizer the HTTP exchange builder customizer
*/
public AllureGrpc(
final AllureLifecycle lifecycle,
final boolean markStepFailedOnNonZeroCode,
final boolean interceptResponseMetadata,
final Consumer<HttpExchange.Builder> exchangeCustomizer) {
this(
lifecycle,
markStepFailedOnNonZeroCode,
false,
interceptResponseMetadata,
exchangeCustomizer
);
}

/**
* Creates an Allure grpc with the supplied values.
*
* @param lifecycle the Allure lifecycle to use
* @param markStepFailedOnNonZeroCode the mark step failed on non zero code
* @param interceptRequestMetadata the intercept request metadata
* @param interceptResponseMetadata the intercept response metadata
* @param exchangeCustomizer the HTTP exchange builder customizer
*/
public AllureGrpc(
final AllureLifecycle lifecycle,
final boolean markStepFailedOnNonZeroCode,
final boolean interceptRequestMetadata,
final boolean interceptResponseMetadata,
final Consumer<HttpExchange.Builder> exchangeCustomizer) {
this.lifecycle = lifecycle;
this.markStepFailedOnNonZeroCode = markStepFailedOnNonZeroCode;
this.interceptRequestMetadata = interceptRequestMetadata;
this.interceptResponseMetadata = interceptResponseMetadata;
this.exchangeCustomizer = exchangeCustomizer == null ? builder -> {
} : exchangeCustomizer;
Expand All @@ -143,6 +191,7 @@ public <T, R> ClientCall<T, R> interceptCall(
final long start = System.currentTimeMillis();
final List<String> clientMessages = new ArrayList<>();
final List<String> serverMessages = new ArrayList<>();
final Map<String, String> capturedRequestHeaders = new LinkedHashMap<>();
final Map<String, String> initialHeaders = new LinkedHashMap<>();
final Map<String, String> trailers = new LinkedHashMap<>();
final String authority = channel.authority();
Expand All @@ -164,6 +213,7 @@ public <T, R> ClientCall<T, R> interceptCall(
) {
@Override
public void start(final Listener<R> responseListener, final Metadata requestHeaders) {
handleRequestHeaders(requestHeaders, capturedRequestHeaders);
final Listener<R> forwardingListener = new ForwardingClientCallListener<R>() {
@Override
protected Listener<R> delegate() {
Expand All @@ -184,7 +234,7 @@ public void onMessage(final R message) {

@Override
public void onClose(final io.grpc.Status status, final Metadata responseTrailers) {
handleClose(status, responseTrailers, stepContext);
handleClose(status, responseTrailers, stepContext, capturedRequestHeaders);
super.onClose(status, responseTrailers);
}
};
Expand All @@ -202,12 +252,13 @@ public void sendMessage(final T message) {
private void handleClose(
final io.grpc.Status status,
final Metadata responseTrailers,
final StepContext<?, ?> stepContext) {
final StepContext<?, ?> stepContext,
final Map<String, String> requestHeaders) {
try {
if (interceptResponseMetadata && responseTrailers != null) {
copyAsciiResponseMetadata(responseTrailers, stepContext.getTrailers());
copyAsciiMetadata(responseTrailers, stepContext.getTrailers());
}
attachExchange(stepContext, status);
attachExchange(stepContext, status, requestHeaders);
stepContext.getLifecycle().updateStep(
stepContext.getStepUuid(),
step -> step.setStatus(convertStatus(status))
Expand All @@ -226,13 +277,23 @@ private void handleClose(
private void handleHeaders(final Metadata headers, final Map<String, String> destination) {
try {
if (interceptResponseMetadata && headers != null) {
copyAsciiResponseMetadata(headers, destination);
copyAsciiMetadata(headers, destination);
}
} catch (Throwable throwable) {
LOGGER.warn("Failed to capture response headers", throwable);
}
}

private void handleRequestHeaders(final Metadata headers, final Map<String, String> destination) {
try {
if (interceptRequestMetadata && headers != null) {
copyAsciiMetadata(headers, destination);
}
} catch (Throwable throwable) {
LOGGER.warn("Failed to capture request headers", throwable);
}
}

private <T> void handleClientMessage(final T message, final List<String> destination) {
try {
destination.add(GRPC_TO_JSON_PRINTER.print((MessageOrBuilder) message));
Expand All @@ -253,10 +314,14 @@ private <R> void handleServerMessage(final R message, final List<String> destina
}
}

private void attachExchange(final StepContext<?, ?> stepContext, final io.grpc.Status status) {
private void attachExchange(
final StepContext<?, ?> stepContext,
final io.grpc.Status status,
final Map<String, String> requestHeaders) {
final HttpExchangeRequest request = buildRequest(
stepContext.getMethodDescriptor(),
stepContext.getClientMessages(),
requestHeaders,
stepContext.getAuthority()
);
final HttpExchangeResponse response = buildResponse(
Expand All @@ -283,6 +348,7 @@ private HttpExchange.Builder exchangeBuilder(final HttpExchangeRequest request)
private HttpExchangeRequest buildRequest(
final MethodDescriptor<?, ?> methodDescriptor,
final List<String> clientMessages,
final Map<String, String> requestHeaders,
final String authority) {
final HttpExchangeRequest.Builder builder = HttpExchangeRequest.builder(
HTTP_METHOD,
Expand All @@ -294,6 +360,9 @@ private HttpExchangeRequest buildRequest(
if (authority != null) {
builder.addHeader(":authority", authority);
}
if (interceptRequestMetadata) {
requestHeaders.forEach(builder::addHeader);
}
return builder
.setBody(toHttpBody(clientMessages, isRequestStreaming(methodDescriptor.getType())))
.build();
Expand Down Expand Up @@ -429,9 +498,9 @@ private static boolean isResponseStreaming(final MethodDescriptor.MethodType met
|| methodType == MethodDescriptor.MethodType.BIDI_STREAMING;
}

private static void copyAsciiResponseMetadata(
final Metadata source,
final Map<String, String> target) {
private static void copyAsciiMetadata(
final Metadata source,
final Map<String, String> target) {
for (String key : source.keys()) {
if (key == null) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ForwardingClientCall;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
Expand Down Expand Up @@ -56,6 +62,7 @@ class AllureGrpcTest {

private static final String RESPONSE_MESSAGE = "Hello world!";
private static final String GRPC_EXCHANGE = "gRPC exchange";
private static final Metadata.Key<String> REQUEST_ID_HEADER = Metadata.Key.of("x-request-id", Metadata.ASCII_STRING_MARSHALLER);
private static final ObjectMapper JSON = new ObjectMapper();

private ManagedChannel managedChannel;
Expand Down Expand Up @@ -318,6 +325,42 @@ void unaryRequestBodyIsCapturedAsJsonObject() throws Exception {
assertThat(exchange.at("/request/body/contentType").asText()).isEqualTo("application/grpc+json");
}

@Test
void unaryRequestMetadataIsCapturedWhenEnabled() throws Exception {
Request request = Request.newBuilder().setTopic("topic-with-metadata").build();
Metadata requestHeaders = new Metadata();
requestHeaders.put(REQUEST_ID_HEADER, "request-42");

AllureResults allureResults = executeUnaryWithMetadata(
request,
requestHeaders,
true
);

JsonNode exchange = readGrpcExchangeAttachment(allureResults);

assertThat(findValueByName(exchange.at("/request/headers"), REQUEST_ID_HEADER.name()))
.contains("request-42");
}

@Test
void unaryRequestMetadataIsNotCapturedWhenDisabled() throws Exception {
Request request = Request.newBuilder().setTopic("topic-without-metadata").build();
Metadata requestHeaders = new Metadata();
requestHeaders.put(REQUEST_ID_HEADER, "request-42");

AllureResults allureResults = executeUnaryWithMetadata(
request,
requestHeaders,
false
);

JsonNode exchange = readGrpcExchangeAttachment(allureResults);

assertThat(findValueByName(exchange.at("/request/headers"), REQUEST_ID_HEADER.name()))
.isEmpty();
}

@Test
void unaryResponseBodyIsCapturedAsJsonObject() throws Exception {
GrpcMock.stubFor(
Expand Down Expand Up @@ -473,4 +516,53 @@ protected final AllureResults executeUnaryExpectingException(Request request) {
);
}

protected final AllureResults executeUnaryWithMetadata(
final Request request,
final Metadata requestHeaders,
final boolean interceptRequestMetadata) {
return Allure.step(
"Execute unary gRPC request with custom metadata and collect Allure results",
() -> runWithinTestContext(() -> {
try {
AllureGrpc interceptor = new RequestMetadataAllureGrpc(
requestHeaders,
interceptRequestMetadata
);
TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(managedChannel)
.withInterceptors(interceptor);
Response response = stub.calculate(request);
assertThat(response.getMessage()).isEqualTo(RESPONSE_MESSAGE);
} catch (Exception exception) {
throw new RuntimeException("Could not execute request " + request, exception);
}
})
);
}

private static final class RequestMetadataAllureGrpc extends AllureGrpc {

private final Metadata requestHeaders;

RequestMetadataAllureGrpc(final Metadata requestHeaders, final boolean interceptRequestMetadata) {
super(Allure.getLifecycle(), true, interceptRequestMetadata, false);
this.requestHeaders = new Metadata();
this.requestHeaders.merge(requestHeaders);
}

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
final MethodDescriptor<ReqT, RespT> method,
final CallOptions callOptions,
final Channel next) {
final ClientCall<ReqT, RespT> delegate = super.interceptCall(method, callOptions, next);
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(delegate) {
@Override
public void start(final Listener<RespT> responseListener, final Metadata headers) {
headers.merge(requestHeaders);
super.start(responseListener, headers);
}
};
}
}

}
Loading