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
35 changes: 27 additions & 8 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.83.1 and Protobuf Java 4.35.1.

## Installation

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

For advanced capture policy, use the constructor that accepts an HTTP exchange builder customizer:
Request and response metadata are captured by default. Cookie metadata is represented as structured HTTP exchange
cookies, so header and cookie redaction can be configured through the HTTP exchange capture policy.

Use the builder to add application-specific header and cookie redaction:

```java
ClientInterceptor allure = AllureGrpc.builder()
.redactHeader("x-api-key")
.redactCookie("session")
.build();
```

Metadata capture can be disabled independently for either direction:

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

For other HTTP exchange capture options, configure the underlying exchange builder:

```java
ClientInterceptor allure = AllureGrpc.builder()
.configureExchange(exchange -> exchange.setMaxBodySize(256_000))
.build();
```

## Report Output

- gRPC method calls as Allure steps.
- Request and response messages, metadata, status, and timing.
- Repeated metadata values in their original order; binary metadata values are Base64-encoded.
- Stream metadata for unary and streaming calls where available.
340 changes: 279 additions & 61 deletions allure-grpc/src/main/java/io/qameta/allure/grpc/AllureGrpc.java

Large diffs are not rendered by default.

306 changes: 303 additions & 3 deletions allure-grpc/src/test/java/io/qameta/allure/grpc/AllureGrpcTest.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.qameta.allure.http;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

final class HttpExchangeCookieParser {

private static final String COOKIE_HEADER = "cookie";
private static final String SET_COOKIE_HEADER = "set-cookie";
private static final String COOKIE_SEPARATOR = ";";
private static final String TOKEN_SEPARATORS = "()<>@,;:\\\"/[]?={}";

private HttpExchangeCookieParser() {
throw new IllegalStateException("Utility class");
}

static boolean isCookieHeader(final String name) {
return COOKIE_HEADER.equalsIgnoreCase(name);
}

static boolean isSetCookieHeader(final String name) {
return SET_COOKIE_HEADER.equalsIgnoreCase(name);
}

static Optional<List<HttpExchangeCookie>> parseCookieHeader(final String value) {
final List<HttpExchangeCookie> result = new ArrayList<>();
for (String part : value.split(COOKIE_SEPARATOR, -1)) {
final Optional<HttpExchangeCookie> cookie = parseCookiePair(part);
if (cookie.isEmpty()) {
return Optional.empty();
}
result.add(cookie.orElseThrow());
}
return result.isEmpty() ? Optional.empty() : Optional.of(List.copyOf(result));
}

static Optional<HttpExchangeCookie> parseSetCookieHeader(final String value) {
final String[] parts = value.split(COOKIE_SEPARATOR, -1);
final Optional<HttpExchangeCookie> cookie = parseCookiePair(parts[0]);
if (cookie.isEmpty()) {
return Optional.empty();
}

final CookieAttributes attributes = new CookieAttributes();
for (int index = 1; index < parts.length; index++) {
if (!attributes.add(parts[index])) {
return Optional.empty();
}
}

return Optional.of(attributes.toCookie(cookie.orElseThrow()));
}

private static Optional<HttpExchangeCookie> parseCookiePair(final String value) {
final int separator = value.indexOf('=');
if (separator <= 0) {
return Optional.empty();
}
final String name = value.substring(0, separator).trim();
final String cookieValue = value.substring(separator + 1).trim();
if (!isToken(name) || !isCookieValue(cookieValue)) {
return Optional.empty();
}
return Optional.of(new HttpExchangeCookie(name, cookieValue));
}

private static boolean isToken(final String value) {
if (value.isEmpty()) {
return false;
}
for (int index = 0; index < value.length(); index++) {
final char character = value.charAt(index);
if (character <= ' ' || character >= '\u007f' || TOKEN_SEPARATORS.indexOf(character) >= 0) {
return false;
}
}
return true;
}

private static boolean isCookieValue(final String value) {
final boolean quoted = value.length() >= 2 && value.charAt(0) == '"'
&& value.charAt(value.length() - 1) == '"';
final int start = quoted ? 1 : 0;
final int end = quoted ? value.length() - 1 : value.length();
for (int index = start; index < end; index++) {
if (!isCookieOctet(value.charAt(index))) {
return false;
}
}
return quoted || value.indexOf('"') < 0;
}

private static boolean isCookieOctet(final char character) {
return character == '!'
|| character >= '#' && character <= '+'
|| character >= '-' && character <= ':'
|| character >= '<' && character <= '['
|| character >= ']' && character <= '~';
}

private static final class CookieAttributes {
private final Set<String> seen = new HashSet<>();
private String path;
private String domain;
private String expires;
private Boolean httpOnly;
private Boolean secure;
private String sameSite;

boolean add(final String rawAttribute) {
final String attribute = rawAttribute.trim();
final int separator = attribute.indexOf('=');
final String name = (separator < 0 ? attribute : attribute.substring(0, separator)).trim();
if (!isToken(name)) {
return false;
}

final String normalizedName = name.toLowerCase(Locale.ROOT);
if (!seen.add(normalizedName)) {
return false;
}

final String value = separator < 0 ? null : attribute.substring(separator + 1).trim();
return switch (normalizedName) {
case "path" -> setPath(value);
case "domain" -> setDomain(value);
case "expires" -> setExpires(value);
case "httponly" -> setHttpOnly(value);
case "secure" -> setSecure(value);
case "samesite" -> setSameSite(value);
default -> false;
};
}

HttpExchangeCookie toCookie(final HttpExchangeCookie cookie) {
return new HttpExchangeCookie(
cookie.name(),
cookie.value(),
path,
domain,
expires,
httpOnly,
secure,
sameSite
);
}

private boolean setPath(final String value) {
path = value;
return value != null;
}

private boolean setDomain(final String value) {
domain = value;
return value != null;
}

private boolean setExpires(final String value) {
expires = value;
return value != null;
}

private boolean setHttpOnly(final String value) {
httpOnly = value == null ? true : null;
return value == null;
}

private boolean setSecure(final String value) {
secure = value == null ? true : null;
return value == null;
}

private boolean setSameSite(final String value) {
sameSite = value;
return value != null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* HTTP request captured in an exchange attachment.
Expand Down Expand Up @@ -76,13 +77,34 @@ public Builder setHttpVersion(final String httpVersion) {
return this;
}

/**
* Adds a request header, converting a valid Cookie header to structured cookies.
*
* @param name the header name
* @param value the header value
* @return this builder
*/
public Builder addHeader(final String name, final String value) {
headers.add(new HttpExchangeNameValue(name, value));
final HttpExchangeNameValue header = new HttpExchangeNameValue(name, value);
if (HttpExchangeCookieParser.isCookieHeader(name)) {
final Optional<List<HttpExchangeCookie>> parsed = HttpExchangeCookieParser.parseCookieHeader(value);
if (parsed.isPresent()) {
cookies.addAll(parsed.orElseThrow());
return this;
}
}
headers.add(header);
return this;
}

/**
* Adds request headers, converting every valid Cookie header to structured cookies.
*
* @param headers the headers to add
* @return this builder
*/
public Builder addHeaders(final List<HttpExchangeNameValue> headers) {
this.headers.addAll(headers);
headers.forEach(header -> addHeader(header.name(), header.value()));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* HTTP response captured in an exchange attachment.
Expand Down Expand Up @@ -78,13 +79,26 @@ public Builder setHttpVersion(final String httpVersion) {
return this;
}

/**
* Adds a response header, converting a valid Set-Cookie header to a structured cookie.
*
* @param name the header name
* @param value the header value
* @return this builder
*/
public Builder addHeader(final String name, final String value) {
headers.add(new HttpExchangeNameValue(name, value));
addHeaderOrCookie(headers, name, value);
return this;
}

/**
* Adds response headers, converting every valid Set-Cookie header to a structured cookie.
*
* @param headers the headers to add
* @return this builder
*/
public Builder addHeaders(final List<HttpExchangeNameValue> headers) {
this.headers.addAll(headers);
headers.forEach(header -> addHeader(header.name(), header.value()));
return this;
}

Expand All @@ -93,13 +107,25 @@ public Builder addCookie(final String name, final String value) {
return this;
}

public Builder addCookies(final List<HttpExchangeCookie> cookies) {
this.cookies.addAll(cookies);
return this;
}

public Builder setBody(final HttpExchangeBody body) {
this.body = body;
return this;
}

/**
* Adds a response trailer, converting a valid Set-Cookie field to a structured cookie.
*
* @param name the trailer name
* @param value the trailer value
* @return this builder
*/
public Builder addTrailer(final String name, final String value) {
trailers.add(new HttpExchangeNameValue(name, value));
addHeaderOrCookie(trailers, name, value);
return this;
}

Expand All @@ -118,5 +144,20 @@ body, nullIfEmpty(trailers), nullIfEmpty(informationalResponses)
private static <T> List<T> nullIfEmpty(final List<T> values) {
return values.isEmpty() ? null : values;
}

private void addHeaderOrCookie(
final List<HttpExchangeNameValue> destination,
final String name,
final String value) {
final HttpExchangeNameValue header = new HttpExchangeNameValue(name, value);
if (HttpExchangeCookieParser.isSetCookieHeader(name)) {
final Optional<HttpExchangeCookie> parsed = HttpExchangeCookieParser.parseSetCookieHeader(value);
if (parsed.isPresent()) {
cookies.add(parsed.orElseThrow());
return;
}
}
destination.add(header);
}
}
}
Loading