Skip to content

Commit 253de0a

Browse files
committed
test: Cover gzip content encoding end to end
Drives both directions over a real socket against the existing openapi.json fixture: `text-echo` echoes its body, so one call exercises request inflation and response coding together, and the spec resource route covers the streamed path. `java.net.http.HttpClient` neither sends `Accept-Encoding` nor decodes a coded response, so the tests set the header themselves and read bytes. That also makes `responseIsNotGzippedWithoutAcceptEncoding` the guard proving compression stays invisible to every other integration test.
1 parent bc8b2ef commit 253de0a

2 files changed

Lines changed: 308 additions & 1 deletion

File tree

docs/plans/dynamic-discovering-piglet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ as it completes.
284284

285285
### Task 5 — end to end
286286

287-
- [ ] **Step 10** `GzipIT` extending `ServerBaseTest`. Reuses the existing `/openapi.json` fixture
287+
- [x] **Step 10** `GzipIT` extending `ServerBaseTest`. Reuses the existing `/openapi.json` fixture
288288
with runtime handler overrides — **no new spec files**. `text-echo` (`POST /text-echo`,
289289
`text/plain`, schema `{"type":"string"}`, no `maxLength`) echoes the body, so one call
290290
exercises both directions. Private `gzip(byte[])` / `gunzip(byte[])` helpers; requests built
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
package com.retailsvc.http;
2+
3+
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
4+
import static java.net.HttpURLConnection.HTTP_ENTITY_TOO_LARGE;
5+
import static java.net.HttpURLConnection.HTTP_OK;
6+
import static java.net.HttpURLConnection.HTTP_UNSUPPORTED_TYPE;
7+
import static java.nio.charset.StandardCharsets.UTF_8;
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import com.retailsvc.http.start.TextEchoHandler;
11+
import java.io.ByteArrayInputStream;
12+
import java.io.ByteArrayOutputStream;
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
import java.net.URI;
16+
import java.net.http.HttpRequest;
17+
import java.net.http.HttpRequest.BodyPublishers;
18+
import java.net.http.HttpResponse;
19+
import java.net.http.HttpResponse.BodyHandlers;
20+
import java.util.Map;
21+
import java.util.function.UnaryOperator;
22+
import java.util.zip.GZIPInputStream;
23+
import java.util.zip.GZIPOutputStream;
24+
import org.junit.jupiter.api.Test;
25+
26+
/** End-to-end coverage of gzip request decoding and response coding over a real socket. */
27+
class GzipIT extends ServerBaseTest {
28+
29+
private static final String TEXT_PLAIN = "text/plain";
30+
private static final String CONTENT_ENCODING = "Content-Encoding";
31+
private static final String ACCEPT_ENCODING = "Accept-Encoding";
32+
private static final String CONTENT_LENGTH = "Content-Length";
33+
34+
// -- request decoding --
35+
36+
@Test
37+
void gzippedRequestBodyIsDecompressed() throws Exception {
38+
try (var s = echoServer();
39+
var client = httpClient()) {
40+
var request =
41+
textEcho(s, gzip("hello gzip".getBytes(UTF_8))).header(CONTENT_ENCODING, "gzip").build();
42+
43+
var response = client.send(request, BodyHandlers.ofString());
44+
45+
assertThat(response.statusCode()).isEqualTo(HTTP_OK);
46+
assertThat(response.body()).isEqualTo("hello gzip");
47+
}
48+
}
49+
50+
@Test
51+
void identityCodedRequestBodyIsUnaffected() throws Exception {
52+
try (var s = echoServer();
53+
var client = httpClient()) {
54+
var request =
55+
textEcho(s, "plain".getBytes(UTF_8)).header(CONTENT_ENCODING, "identity").build();
56+
57+
var response = client.send(request, BodyHandlers.ofString());
58+
59+
assertThat(response.statusCode()).isEqualTo(HTTP_OK);
60+
assertThat(response.body()).isEqualTo("plain");
61+
}
62+
}
63+
64+
@Test
65+
void handlerDoesNotSeeContentEncodingHeader() throws Exception {
66+
RequestHandler reportsEncoding =
67+
req -> Response.text(HTTP_OK, req.header(CONTENT_ENCODING).orElse("absent"));
68+
try (var s = serverWith(Map.of("text-echo", reportsEncoding));
69+
var client = httpClient()) {
70+
var request =
71+
textEcho(s, gzip("body".getBytes(UTF_8))).header(CONTENT_ENCODING, "gzip").build();
72+
73+
var response = client.send(request, BodyHandlers.ofString());
74+
75+
assertThat(response.body()).isEqualTo("absent");
76+
}
77+
}
78+
79+
@Test
80+
void unsupportedRequestEncodingReturns415() throws Exception {
81+
try (var s = echoServer();
82+
var client = httpClient()) {
83+
var request = textEcho(s, "body".getBytes(UTF_8)).header(CONTENT_ENCODING, "br").build();
84+
85+
var response = client.send(request, BodyHandlers.ofString());
86+
87+
assertThat(response.statusCode()).isEqualTo(HTTP_UNSUPPORTED_TYPE);
88+
assertThat(response.headers().firstValue("Content-Type"))
89+
.contains("application/problem+json");
90+
assertThat(response.body()).contains("Unsupported Media Type");
91+
}
92+
}
93+
94+
@Test
95+
void malformedGzipRequestReturns400() throws Exception {
96+
try (var s = echoServer();
97+
var client = httpClient()) {
98+
var request =
99+
textEcho(s, "not gzip at all".getBytes(UTF_8)).header(CONTENT_ENCODING, "gzip").build();
100+
101+
var response = client.send(request, BodyHandlers.ofString());
102+
103+
assertThat(response.statusCode()).isEqualTo(HTTP_BAD_REQUEST);
104+
assertThat(response.body()).contains("malformed gzip request body");
105+
}
106+
}
107+
108+
@Test
109+
void oversizedGzipRequestReturns413() throws Exception {
110+
try (var s = echoServer(builder -> builder.maxDecompressedRequestBytes(1024));
111+
var client = httpClient()) {
112+
var request = textEcho(s, gzip(new byte[8192])).header(CONTENT_ENCODING, "gzip").build();
113+
114+
var response = client.send(request, BodyHandlers.ofString());
115+
116+
assertThat(response.statusCode()).isEqualTo(HTTP_ENTITY_TOO_LARGE);
117+
assertThat(response.body()).contains("Content Too Large");
118+
}
119+
}
120+
121+
// -- response coding --
122+
123+
@Test
124+
void largeResponseIsGzippedWhenClientAcceptsGzip() throws Exception {
125+
String payload = "compress me please ".repeat(200);
126+
try (var s = echoServer();
127+
var client = httpClient()) {
128+
var request = textEcho(s, payload.getBytes(UTF_8)).header(ACCEPT_ENCODING, "gzip").build();
129+
130+
HttpResponse<byte[]> response = client.send(request, BodyHandlers.ofByteArray());
131+
132+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).contains("gzip");
133+
assertThat(response.headers().firstValue("Vary"))
134+
.hasValueSatisfying(vary -> assertThat(vary).contains(ACCEPT_ENCODING));
135+
assertThat(new String(gunzip(response.body()), UTF_8)).isEqualTo(payload);
136+
assertThat(response.body().length).isLessThan(payload.length());
137+
}
138+
}
139+
140+
@Test
141+
void responseIsNotGzippedWithoutAcceptEncoding() throws Exception {
142+
String payload = "compress me please ".repeat(200);
143+
try (var s = echoServer();
144+
var client = httpClient()) {
145+
var request = textEcho(s, payload.getBytes(UTF_8)).build();
146+
147+
var response = client.send(request, BodyHandlers.ofString());
148+
149+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).isEmpty();
150+
assertThat(response.body()).isEqualTo(payload);
151+
}
152+
}
153+
154+
@Test
155+
void smallResponseIsNotGzipped() throws Exception {
156+
try (var s = echoServer();
157+
var client = httpClient()) {
158+
var request = textEcho(s, "tiny".getBytes(UTF_8)).header(ACCEPT_ENCODING, "gzip").build();
159+
160+
var response = client.send(request, BodyHandlers.ofString());
161+
162+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).isEmpty();
163+
assertThat(response.body()).isEqualTo("tiny");
164+
}
165+
}
166+
167+
@Test
168+
void gzipRefusedByQValueIsNotApplied() throws Exception {
169+
String payload = "compress me please ".repeat(200);
170+
try (var s = echoServer();
171+
var client = httpClient()) {
172+
var request =
173+
textEcho(s, payload.getBytes(UTF_8)).header(ACCEPT_ENCODING, "gzip;q=0").build();
174+
175+
var response = client.send(request, BodyHandlers.ofString());
176+
177+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).isEmpty();
178+
assertThat(response.body()).isEqualTo(payload);
179+
}
180+
}
181+
182+
// -- streamed extra routes --
183+
184+
@Test
185+
void streamedSpecResourceIsGzippedAndChunked() throws Exception {
186+
try (var s = specServer();
187+
var client = httpClient()) {
188+
var request = specRequest(s).header(ACCEPT_ENCODING, "gzip").GET().build();
189+
190+
HttpResponse<byte[]> response = client.send(request, BodyHandlers.ofByteArray());
191+
192+
assertThat(response.statusCode()).isEqualTo(HTTP_OK);
193+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).contains("gzip");
194+
assertThat(response.headers().firstValue(CONTENT_LENGTH)).isEmpty();
195+
assertThat(gunzip(response.body())).isEqualTo(classpathBytes());
196+
}
197+
}
198+
199+
@Test
200+
void streamedSpecResourceIsPlainWithoutAcceptEncoding() throws Exception {
201+
try (var s = specServer();
202+
var client = httpClient()) {
203+
var request = specRequest(s).GET().build();
204+
205+
HttpResponse<byte[]> response = client.send(request, BodyHandlers.ofByteArray());
206+
207+
assertThat(response.headers().firstValue(CONTENT_ENCODING)).isEmpty();
208+
assertThat(response.body()).isEqualTo(classpathBytes());
209+
}
210+
}
211+
212+
@Test
213+
void headOmitsContentLengthWhenGetWouldBeCompressed() throws Exception {
214+
try (var s = specServer();
215+
var client = httpClient()) {
216+
var request =
217+
specRequest(s)
218+
.header(ACCEPT_ENCODING, "gzip")
219+
.method("HEAD", BodyPublishers.noBody())
220+
.build();
221+
222+
var response = client.send(request, BodyHandlers.ofString());
223+
224+
assertThat(response.statusCode()).isEqualTo(HTTP_OK);
225+
assertThat(response.headers().firstValue(CONTENT_LENGTH)).isEmpty();
226+
assertThat(response.headers().firstValue("Content-Type")).contains("application/yaml");
227+
}
228+
}
229+
230+
@Test
231+
void headKeepsContentLengthWithoutAcceptEncoding() throws Exception {
232+
try (var s = specServer();
233+
var client = httpClient()) {
234+
var request = specRequest(s).method("HEAD", BodyPublishers.noBody()).build();
235+
236+
var response = client.send(request, BodyHandlers.ofString());
237+
238+
assertThat(response.headers().firstValue(CONTENT_LENGTH))
239+
.contains(String.valueOf(classpathBytes().length));
240+
}
241+
}
242+
243+
// -- fixtures --
244+
245+
private OpenApiServer echoServer() {
246+
return echoServer(builder -> builder);
247+
}
248+
249+
private OpenApiServer echoServer(UnaryOperator<OpenApiServer.Builder> customise) {
250+
return serverWith(Map.of("text-echo", new TextEchoHandler()), customise);
251+
}
252+
253+
private OpenApiServer serverWith(Map<String, RequestHandler> handlers) {
254+
return serverWith(handlers, builder -> builder);
255+
}
256+
257+
private OpenApiServer serverWith(
258+
Map<String, RequestHandler> handlers, UnaryOperator<OpenApiServer.Builder> customise) {
259+
try {
260+
server =
261+
customise
262+
.apply(newBuilder().spec(spec).handlers(stubAllHandlers(handlers)).port(0))
263+
.build();
264+
return server;
265+
} catch (IOException e) {
266+
throw new IllegalStateException(e);
267+
}
268+
}
269+
270+
private OpenApiServer specServer() {
271+
return serverWith(
272+
Map.of(),
273+
builder -> builder.extraRoute("/openapi.yaml", Handlers.resourceHandler("/openapi.yaml")));
274+
}
275+
276+
private HttpRequest.Builder textEcho(OpenApiServer server, byte[] body) {
277+
return HttpRequest.newBuilder()
278+
.uri(URI.create("http://localhost:%d/api/v1/text-echo".formatted(server.listenPort())))
279+
.header("Content-Type", TEXT_PLAIN)
280+
.POST(BodyPublishers.ofByteArray(body));
281+
}
282+
283+
private HttpRequest.Builder specRequest(OpenApiServer server) {
284+
return HttpRequest.newBuilder()
285+
.uri(URI.create("http://localhost:%d/openapi.yaml".formatted(server.listenPort())));
286+
}
287+
288+
private static byte[] classpathBytes() throws IOException {
289+
try (InputStream in = GzipIT.class.getResourceAsStream("/openapi.yaml")) {
290+
return in.readAllBytes();
291+
}
292+
}
293+
294+
private static byte[] gzip(byte[] data) throws IOException {
295+
ByteArrayOutputStream out = new ByteArrayOutputStream();
296+
try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
297+
gzip.write(data);
298+
}
299+
return out.toByteArray();
300+
}
301+
302+
private static byte[] gunzip(byte[] data) throws IOException {
303+
try (GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(data))) {
304+
return in.readAllBytes();
305+
}
306+
}
307+
}

0 commit comments

Comments
 (0)