Skip to content

Commit f40c8c6

Browse files
committed
Add openfeign module
1 parent aad4638 commit f40c8c6

5 files changed

Lines changed: 278 additions & 0 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,24 @@ final HttpClientBuilder builder = HttpClientBuilder.create()
195195
.addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl"));
196196
```
197197

198+
## OpenFeign
199+
OpenFeign wrapper over decoder for automatically captures traffic as Allure attachments for comprehensive API test reporting.
200+
```xml
201+
<dependency>
202+
<groupId>io.qameta.allure</groupId>
203+
<artifactId>allure-open-feign</artifactId>
204+
<version>$LATEST_VERSION</version>
205+
</dependency>
206+
```
207+
208+
Usage example with GsonDecoder implementation:
209+
```java
210+
MyClient myClient = Feign.builder()
211+
.decoder(new AllureResponseDecoder(new GsonDecoder()))
212+
.encoder(new GsonEncoder())
213+
.target(MyClient.class, "https://test.url");
214+
```
215+
198216
## JAX-RS Filter
199217

200218
Filter that can be used with JAX-RS compliant clients such as RESTeasy and Jersey

allure-openfeign/build.gradle.kts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
description = "Allure OpenFeign Integration"
2+
3+
dependencies {
4+
implementation("io.github.openfeign:feign-core:13.6")
5+
testImplementation("io.github.openfeign:feign-gson:13.6")
6+
api(project(":allure-attachments"))
7+
testImplementation("com.github.tomakehurst:wiremock")
8+
testImplementation("org.assertj:assertj-core")
9+
testImplementation("org.jboss.resteasy:resteasy-client")
10+
testImplementation("org.junit.jupiter:junit-jupiter-api")
11+
testImplementation("org.mockito:mockito-core")
12+
testImplementation("org.slf4j:slf4j-simple")
13+
testImplementation(project(":allure-java-commons-test"))
14+
testImplementation(project(":allure-junit-platform"))
15+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
16+
}
17+
18+
tasks.jar {
19+
manifest {
20+
attributes(mapOf(
21+
"Automatic-Module-Name" to "io.qameta.allure.openfeign"
22+
))
23+
}
24+
}
25+
26+
tasks.test {
27+
useJUnitPlatform()
28+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2016-2024 Qameta Software Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.openfeign;
17+
18+
import feign.Request;
19+
import feign.Response;
20+
import feign.codec.DecodeException;
21+
import feign.codec.Decoder;
22+
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
23+
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
24+
import io.qameta.allure.attachment.http.HttpRequestAttachment;
25+
import io.qameta.allure.attachment.http.HttpResponseAttachment;
26+
27+
import java.io.ByteArrayOutputStream;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.lang.reflect.Type;
31+
import java.nio.charset.Charset;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.Collection;
34+
import java.util.HashMap;
35+
import java.util.Map;
36+
import java.util.Objects;
37+
import java.util.stream.Collectors;
38+
39+
/**
40+
* @author sbushmelev (Sergei Bushmelev)
41+
*/
42+
public class AllureResponseDecoder implements Decoder {
43+
44+
private final Decoder decoder;
45+
46+
/**
47+
* Creates a new AllureResponseDecoder wrapping the specified decoder.
48+
*
49+
* @param decoder the underlying decoder to delegate actual decoding to
50+
*/
51+
public AllureResponseDecoder(final Decoder decoder) {
52+
this.decoder = decoder;
53+
}
54+
55+
@Override
56+
public Object decode(final Response response, final Type type) throws IOException {
57+
final Request request = response.request();
58+
59+
final HttpRequestAttachment.Builder requestAttachmentBuilder = HttpRequestAttachment
60+
.Builder.create("Request", request.url())
61+
.setMethod(request.httpMethod().name())
62+
.setHeaders(headers(request.headers()));
63+
64+
if (Objects.nonNull(request.body())) {
65+
final Charset charset = request.charset() == null ? StandardCharsets.UTF_8 : request.charset();
66+
requestAttachmentBuilder.setBody(new String(request.body(), charset));
67+
}
68+
69+
new DefaultAttachmentProcessor().addAttachment(
70+
requestAttachmentBuilder.build(),
71+
new FreemarkerAttachmentRenderer("http-request.ftl")
72+
);
73+
74+
final HttpResponseAttachment.Builder responseAttachmentBuilder = HttpResponseAttachment
75+
.Builder.create("Response")
76+
.setResponseCode(response.status())
77+
.setHeaders(headers(response.headers()));
78+
79+
final Response.Builder builder = response.toBuilder();
80+
81+
if (Objects.nonNull(response.body())) {
82+
try (InputStream bodyStream = response.body().asInputStream()) {
83+
final byte[] body = readAllBytes(bodyStream);
84+
final Charset charset = response.charset() == null ? StandardCharsets.UTF_8 : response.charset();
85+
responseAttachmentBuilder.setBody(new String(body, charset));
86+
builder.body(body);
87+
} catch (IOException e) {
88+
throw new DecodeException(response.status(), "Failed to read response body", request, e);
89+
}
90+
}
91+
92+
new DefaultAttachmentProcessor().addAttachment(
93+
responseAttachmentBuilder.build(),
94+
new FreemarkerAttachmentRenderer("http-response.ftl")
95+
);
96+
97+
return decoder.decode(builder.build(), type);
98+
}
99+
100+
private byte[] readAllBytes(final InputStream inputStream) throws IOException {
101+
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
102+
int byteRead;
103+
while ((byteRead = inputStream.read()) != -1) {
104+
buffer.write(byteRead);
105+
}
106+
return buffer.toByteArray();
107+
}
108+
109+
private Map<String, String> headers(final Map<String, Collection<String>> headers) {
110+
if (headers == null) {
111+
return new HashMap<>();
112+
} else {
113+
return headers.entrySet().stream()
114+
.collect(Collectors.toMap(
115+
Map.Entry::getKey,
116+
entry -> "Set-Cookie".equalsIgnoreCase(entry.getKey())
117+
? String.join("\n", entry.getValue())
118+
: String.join(", ", entry.getValue())
119+
));
120+
}
121+
}
122+
123+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2016-2024 Qameta Software Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.openfeign;
17+
18+
import com.github.tomakehurst.wiremock.WireMockServer;
19+
import feign.Feign;
20+
import feign.RequestLine;
21+
import feign.gson.GsonDecoder;
22+
import feign.gson.GsonEncoder;
23+
import io.qameta.allure.model.Attachment;
24+
import io.qameta.allure.test.AllureResults;
25+
import org.junit.jupiter.api.BeforeAll;
26+
import org.junit.jupiter.api.Test;
27+
28+
import java.util.List;
29+
import java.util.concurrent.atomic.AtomicReference;
30+
import java.util.stream.Collectors;
31+
32+
import static io.qameta.allure.test.RunUtils.runWithinTestContext;
33+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
34+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
35+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
36+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
37+
import static org.junit.jupiter.api.Assertions.assertAll;
38+
import static org.junit.jupiter.api.Assertions.assertEquals;
39+
import static org.junit.jupiter.api.Assertions.assertTrue;
40+
41+
public class AllureResponseDecoderTests {
42+
43+
static WireMockServer wireMockServer;
44+
45+
@BeforeAll
46+
static void setUp() {
47+
wireMockServer = new WireMockServer(options().dynamicPort());
48+
wireMockServer.start();
49+
50+
wireMockServer.stubFor(
51+
get(urlEqualTo("/api/v1/json"))
52+
.willReturn(aResponse()
53+
.withStatus(200)
54+
.withHeader("Content-Type", "application/json")
55+
.withBody("{\"message\":\"Hello World\"}")));
56+
}
57+
58+
@Test
59+
void jsonBodyTest() {
60+
AtomicReference<HelloWorldRecord> helloWorldRecord = new AtomicReference<>();
61+
62+
AllureResults allureResults = runWithinTestContext(() -> {
63+
helloWorldRecord.set(Feign.builder()
64+
.decoder(new AllureResponseDecoder(new GsonDecoder()))
65+
.encoder(new GsonEncoder())
66+
.target(HelloWorldFeignClient.class, wireMockServer.baseUrl())
67+
.getJsonHelloWorld());
68+
});
69+
70+
List<String> attachmentNames = allureResults.getTestResults().stream()
71+
.flatMap(testResult -> testResult.getAttachments().stream())
72+
.map(Attachment::getName).collect(Collectors.toList());
73+
74+
assertAll(
75+
() -> assertEquals(new HelloWorldRecord("Hello World").getMessage(), helloWorldRecord.get().getMessage()),
76+
() -> assertTrue(attachmentNames.contains("Response"), "Cannot find attachment with name \"Response\""),
77+
() -> assertTrue(attachmentNames.contains("Request"), "Cannot find attachment with name \"Request\"")
78+
);
79+
}
80+
81+
interface HelloWorldFeignClient {
82+
83+
@RequestLine("GET /api/v1/json")
84+
HelloWorldRecord getJsonHelloWorld();
85+
86+
}
87+
88+
static class HelloWorldRecord {
89+
90+
private String message;
91+
92+
public HelloWorldRecord() {
93+
}
94+
95+
public HelloWorldRecord(String message) {
96+
this.message = message;
97+
}
98+
99+
public String getMessage() {
100+
return message;
101+
}
102+
103+
public void setMessage(String message) {
104+
this.message = message;
105+
}
106+
}
107+
108+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ include("allure-spock2")
4040
include("allure-spring-web")
4141
include("allure-test-filter")
4242
include("allure-testng")
43+
include("allure-openfeign")
4344

4445
pluginManagement {
4546
repositories {

0 commit comments

Comments
 (0)