Skip to content

Commit

Permalink
add interceptors for httpclient5 (via #935)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-simeshin authored Jul 10, 2023
1 parent 10b1bcc commit 466aa0b
Show file tree
Hide file tree
Showing 12 changed files with 888 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ Usage example:
.addInterceptorLast(new AllureHttpClientResponse());
```

## Http client 5
Interceptors for Apache [httpclient5](https://hc.apache.org/httpcomponents-client-5.2.x/index.html).
Additional info can be found in module `allure-httpclient5`

```xml
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-httpclient5</artifactId>
<version>$LATEST_VERSION</version>
</dependency>
```

Usage example:
```java
final HttpClientBuilder builder = HttpClientBuilder.create()
.addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl"))
.addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl"));
```

## JAX-RS Filter

Filter that can be used with JAX-RS compliant clients such as RESTeasy and Jersey
Expand Down
27 changes: 27 additions & 0 deletions allure-httpclient5/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
description = "Allure Apache HttpClient5 Integration"

dependencies {
api(project(":allure-attachments"))
implementation("org.apache.httpcomponents.client5:httpclient5")
testImplementation("com.github.tomakehurst:wiremock")
testImplementation("io.github.glytching:junit-extensions")
testImplementation("org.assertj:assertj-core")
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.mockito:mockito-core")
testImplementation("org.slf4j:slf4j-simple")
testImplementation(project(":allure-java-commons-test"))
testImplementation(project(":allure-junit-platform"))
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.jar {
manifest {
attributes(mapOf(
"Automatic-Module-Name" to "io.qameta.allure.httpclient5"
))
}
}

tasks.test {
useJUnitPlatform()
}
56 changes: 56 additions & 0 deletions allure-httpclient5/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Allure-httpclient5
Extended logging for requests and responses with [httpclient5](https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5)
This library does not support `httpclient` due to package and API changes between `httpclient` and `httpclient5`.
To work with `httpclient`, it is recommended to use the `allure-httpclient` library.

## Wiki
https://hc.apache.org/httpcomponents-client-5.2.x/
https://hc.apache.org/httpcomponents-client-5.2.x/quickstart.html
https://hc.apache.org/httpcomponents-client-5.2.x/migration-guide/index.html
https://hc.apache.org/httpcomponents-client-5.2.x/examples.html

## Additional features
Implemented:
- The `httpclient5` library uses `gzip` compression by default. Interceptors attach message bodies in decompressed form
- `HttpEntityEnclosingRequest` is removed from `httpclient5`. Request interceptor works wo `HttpEntityEnclosingRequest`

Not tested:
- The httpclient5 library support Async interactions (Not tested)

## Examples

```java
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import io.qameta.allure.httpclient5.AllureHttpClient5Request;
import io.qameta.allure.httpclient5.AllureHttpClient5Response;

class Test {

@Test
void smokeGetShouldNotThrowThenReturnCorrectResponseMessage() throws IOException {
final HttpClientBuilder builder = HttpClientBuilder.create()
.addRequestInterceptorFirst(new AllureHttpClient5Request())
.addResponseInterceptorLast(new AllureHttpClient5Response());

try (CloseableHttpClient httpClient = builder.build()) {
final HttpGet httpGet = new HttpGet("/hello");
httpClient.execute(httpGet, response -> {
assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING);
return response;
});
}
}
}
```

In addition to using standard templates for formatting, you can use your custom `ftl` templates along the path
`/resources/tpl/...`. For examples, you can use templates from the `allure-attachments` module.

```java
final HttpClientBuilder builder = HttpClientBuilder.create()
.addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl"))
.addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl"));
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2019 Qameta Software OÜ
*
* 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.httpclient5;

import io.qameta.allure.attachment.AttachmentData;
import io.qameta.allure.attachment.AttachmentProcessor;
import io.qameta.allure.attachment.AttachmentRenderer;
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
import io.qameta.allure.attachment.http.HttpRequestAttachment;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.protocol.HttpContext;

import java.util.stream.Stream;

import static io.qameta.allure.attachment.http.HttpRequestAttachment.Builder.create;

/**
* @author a-simeshin (Simeshin Artem)
*/
@SuppressWarnings("PMD.MethodArgumentCouldBeFinal")
public class AllureHttpClient5Request implements HttpRequestInterceptor {

private final AttachmentRenderer<AttachmentData> renderer;
private final AttachmentProcessor<AttachmentData> processor;

public AllureHttpClient5Request() {
this("http-request.ftl");
}

public AllureHttpClient5Request(final String templateName) {
this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor());
}

public AllureHttpClient5Request(final AttachmentRenderer<AttachmentData> renderer,
final AttachmentProcessor<AttachmentData> processor) {
this.renderer = renderer;
this.processor = processor;
}

/**
* Processes the HTTP request and adds an attachment to the Allure Attachment processor.
*
* @param request the HTTP request
* @param entity the entity details
* @param context the HTTP context
*/
@Override
public void process(HttpRequest request, EntityDetails entity, HttpContext context) {
final String attachmentName = getAttachmentName(request);
final HttpRequestAttachment.Builder builder = create(attachmentName, request.getRequestUri());
builder.setMethod(request.getMethod());

Stream.of(request.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue()));

if (entity instanceof HttpEntity && ((HttpEntity) entity).isRepeatable() && entity.getContentLength() != 0) {
builder.setBody(AllureHttpEntityUtils.getBody((HttpEntity) entity));
}

processor.addAttachment(builder.build(), renderer);
}

private String getAttachmentName(final HttpRequest request) {
return String.format("Request_%s_%s", request.getMethod(), request.getRequestUri());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2019 Qameta Software OÜ
*
* 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.httpclient5;

import io.qameta.allure.attachment.AttachmentData;
import io.qameta.allure.attachment.AttachmentProcessor;
import io.qameta.allure.attachment.AttachmentRenderer;
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
import io.qameta.allure.attachment.http.HttpResponseAttachment;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpResponseInterceptor;
import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;

import java.io.IOException;
import java.util.stream.Stream;

import static io.qameta.allure.attachment.http.HttpResponseAttachment.Builder.create;

/**
* @author a-simeshin (Simeshin Artem)
*/
@SuppressWarnings({
"checkstyle:ParameterAssignment",
"PMD.MethodArgumentCouldBeFinal",
"PMD.AvoidReassigningParameters"})
public class AllureHttpClient5Response implements HttpResponseInterceptor {
private final AttachmentRenderer<AttachmentData> renderer;
private final AttachmentProcessor<AttachmentData> processor;
private static final String NO_BODY = "No body present";

public AllureHttpClient5Response() {
this("http-response.ftl");
}

public AllureHttpClient5Response(final String templateName) {
this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor());
}

public AllureHttpClient5Response(final AttachmentRenderer<AttachmentData> renderer,
final AttachmentProcessor<AttachmentData> processor) {
this.renderer = renderer;
this.processor = processor;
}

/**
* Processes the HTTP response and adds an attachment to the Allure Attachment processor.
*
* @param response the HTTP response
* @param entity the entity details, may be null for no response body responses
* @param context the HTTP context
* @throws IOException if an I/O error occurs
*/
@Override
public void process(HttpResponse response, EntityDetails entity, HttpContext context) throws IOException {
final HttpResponseAttachment.Builder builder = create("Response");
builder.setResponseCode(response.getCode());

Stream.of(response.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue()));

final HttpEntity originalHttpEntity = (HttpEntity) entity;
if (originalHttpEntity != null && !originalHttpEntity.isRepeatable()) {
// Looks like a bug or completely new logic. It's not enough to replace chaining EntityDetails entity.
// To read the response body twice, It needs to put in the context also
entity = new BufferedHttpEntity(originalHttpEntity);
final BasicClassicHttpResponse responseEntity =
(BasicClassicHttpResponse) context.getAttribute("http.response");
responseEntity.setEntity((HttpEntity) entity);

final String responseBody = AllureHttpEntityUtils.getBody((HttpEntity) entity);
if (responseBody == null || responseBody.isEmpty()) {
builder.setBody(NO_BODY);
} else {
builder.setBody(responseBody);
}
} else {
builder.setBody(NO_BODY);
}

processor.addAttachment(builder.build(), renderer);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2023 Qameta Software OÜ
*
* 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.httpclient5;

import io.qameta.allure.AllureResultsWriteException;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;

/**
* Utility class for working with HTTP entity in Allure framework.
*/
@SuppressWarnings({"checkstyle:ParameterAssignment", "PMD.AssignmentInOperand"})
public final class AllureHttpEntityUtils {

private AllureHttpEntityUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}

/**
* Retrieves the body of the HTTP entity as a string.
*
* @param httpEntity the HTTP entity
* @return the body of the HTTP entity as a string
* @throws AllureResultsWriteException if an error occurs while reading the entity body
*/
static String getBody(final HttpEntity httpEntity) {
try {
final String contentEncoding = httpEntity.getContentEncoding();
if (contentEncoding != null && contentEncoding.contains("gzip")) {
return unpackGzipEntityString(httpEntity);
} else {
return EntityUtils.toString(httpEntity, getContentEncoding(httpEntity.getContentEncoding()));
}
} catch (IOException | ParseException e) {
throw new AllureResultsWriteException("Can't read request message body to String", e);
}
}

/**
* Retrieves the content encoding of the HTTP entity.
*
* @param contentEncoding the content encoding value
* @return the charset corresponding to the content encoding, or UTF-8 if the encoding is invalid
*/
static Charset getContentEncoding(final String contentEncoding) {
try {
return Charset.forName(contentEncoding);
} catch (IllegalArgumentException ignored) {
return StandardCharsets.UTF_8;
}
}

/**
* Unpacks the GZIP-encoded entity string.
*
* @param entity the GZIP-encoded HTTP entity
* @return the unpacked entity string
* @throws IOException if an error occurs while unpacking the entity
*/
static String unpackGzipEntityString(final HttpEntity entity) throws IOException {
final GZIPInputStream gis = new GZIPInputStream(entity.getContent());
final Charset contentEncoding = getContentEncoding(entity.getContentEncoding());
try (InputStreamReader inputStreamReader = new InputStreamReader(gis, contentEncoding)) {
try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
final StringBuilder outStr = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
outStr.append(line);
}
return outStr.toString();
}
}
}

}
Loading

0 comments on commit 466aa0b

Please sign in to comment.