Skip to content

Commit 3ed4faa

Browse files
committed
test: Add integration tests for after-response hooks
1 parent 6eba347 commit 3ed4faa

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package com.retailsvc.http;
2+
3+
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
4+
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
5+
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
import com.google.gson.Gson;
9+
import com.retailsvc.http.internal.DispatchHandler;
10+
import com.retailsvc.http.spec.Spec;
11+
import java.io.InputStream;
12+
import java.net.URI;
13+
import java.net.http.HttpClient;
14+
import java.net.http.HttpRequest;
15+
import java.net.http.HttpResponse;
16+
import java.net.http.HttpResponse.BodyHandlers;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import java.util.concurrent.CopyOnWriteArrayList;
22+
import java.util.concurrent.CountDownLatch;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.atomic.AtomicReference;
25+
import org.junit.jupiter.api.Test;
26+
27+
class AfterResponseHookIT {
28+
29+
// get-data accepts GET /data with no required parameters
30+
private static final String OK_OPERATION_ID = "get-data";
31+
private static final String OK_PATH = "/api/v1/data";
32+
private static final String NOT_FOUND_PATH = "/api/v1/does-not-exist";
33+
34+
private static Spec loadSpec() {
35+
Gson gson = new Gson();
36+
try (InputStream in = AfterResponseHookIT.class.getResourceAsStream("/openapi.json")) {
37+
String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
38+
@SuppressWarnings("unchecked")
39+
Map<String, Object> raw = (Map<String, Object>) gson.fromJson(text, Map.class);
40+
return Spec.from(raw);
41+
} catch (Exception e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
private static OpenApiServer.Builder baseBuilder() {
47+
return OpenApiServer.builder()
48+
.spec(loadSpec())
49+
.port(0)
50+
.securityValidator("apiKeyAuth", (req, cred) -> Optional.empty())
51+
.securityValidator("bearerAuth", (req, cred) -> Optional.empty())
52+
.securityValidator("basicAuth", (req, cred) -> Optional.empty());
53+
}
54+
55+
private static URI uri(OpenApiServer server, String path) {
56+
return URI.create("http://localhost:" + server.listenPort() + path);
57+
}
58+
59+
@Test
60+
void globalHookFiresAfterSuccessfulResponse() throws Exception {
61+
AtomicReference<Request> capturedRequest = new AtomicReference<>();
62+
AtomicReference<Response> capturedResponse = new AtomicReference<>();
63+
CountDownLatch latch = new CountDownLatch(1);
64+
65+
try (OpenApiServer server =
66+
baseBuilder()
67+
.handlers(Map.of(OK_OPERATION_ID, req -> Response.status(HTTP_NO_CONTENT)))
68+
.afterResponseHook(
69+
(req, resp) -> {
70+
capturedRequest.set(req);
71+
capturedResponse.set(resp);
72+
latch.countDown();
73+
})
74+
.build()) {
75+
76+
HttpResponse<Void> resp =
77+
HttpClient.newHttpClient()
78+
.send(
79+
HttpRequest.newBuilder(uri(server, OK_PATH)).GET().build(),
80+
BodyHandlers.discarding());
81+
82+
assertThat(resp.statusCode()).isEqualTo(HTTP_NO_CONTENT);
83+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
84+
assertThat(capturedRequest.get()).isNotNull();
85+
assertThat(capturedRequest.get().operationId()).isEqualTo(OK_OPERATION_ID);
86+
assertThat(capturedResponse.get()).isNotNull();
87+
assertThat(capturedResponse.get().status()).isEqualTo(HTTP_NO_CONTENT);
88+
}
89+
}
90+
91+
@Test
92+
void perRequestRunnablesFireInOrder() throws Exception {
93+
List<String> log = new CopyOnWriteArrayList<>();
94+
CountDownLatch latch = new CountDownLatch(2);
95+
96+
try (OpenApiServer server =
97+
baseBuilder()
98+
.handlers(
99+
Map.of(
100+
OK_OPERATION_ID,
101+
req -> {
102+
req.afterResponse(
103+
() -> {
104+
log.add("first");
105+
latch.countDown();
106+
});
107+
req.afterResponse(
108+
() -> {
109+
log.add("second");
110+
latch.countDown();
111+
});
112+
return Response.status(HTTP_NO_CONTENT);
113+
}))
114+
.build()) {
115+
116+
HttpClient.newHttpClient()
117+
.send(
118+
HttpRequest.newBuilder(uri(server, OK_PATH)).GET().build(),
119+
BodyHandlers.discarding());
120+
121+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
122+
assertThat(log).containsExactly("first", "second");
123+
}
124+
}
125+
126+
@Test
127+
void hookExceptionDoesNotAffectClientOrOtherHooks() throws Exception {
128+
List<String> log = new CopyOnWriteArrayList<>();
129+
CountDownLatch latch = new CountDownLatch(1);
130+
131+
try (OpenApiServer server =
132+
baseBuilder()
133+
.handlers(Map.of(OK_OPERATION_ID, req -> Response.status(HTTP_NO_CONTENT)))
134+
.afterResponseHook(
135+
(req, resp) -> {
136+
throw new RuntimeException("boom");
137+
})
138+
.afterResponseHook(
139+
(req, resp) -> {
140+
log.add("second-ran");
141+
latch.countDown();
142+
})
143+
.build()) {
144+
145+
HttpResponse<Void> resp =
146+
HttpClient.newHttpClient()
147+
.send(
148+
HttpRequest.newBuilder(uri(server, OK_PATH)).GET().build(),
149+
BodyHandlers.discarding());
150+
151+
assertThat(resp.statusCode()).isEqualTo(HTTP_NO_CONTENT);
152+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
153+
assertThat(log).containsExactly("second-ran");
154+
}
155+
}
156+
157+
@Test
158+
void hookFiresOnHandlerException() throws Exception {
159+
AtomicReference<Response> capturedResponse = new AtomicReference<>();
160+
CountDownLatch latch = new CountDownLatch(1);
161+
162+
try (OpenApiServer server =
163+
baseBuilder()
164+
.handlers(
165+
Map.of(
166+
OK_OPERATION_ID,
167+
req -> {
168+
throw new RuntimeException("kapow");
169+
}))
170+
.afterResponseHook(
171+
(req, resp) -> {
172+
capturedResponse.set(resp);
173+
latch.countDown();
174+
})
175+
.build()) {
176+
177+
HttpResponse<Void> resp =
178+
HttpClient.newHttpClient()
179+
.send(
180+
HttpRequest.newBuilder(uri(server, OK_PATH)).GET().build(),
181+
BodyHandlers.discarding());
182+
183+
assertThat(resp.statusCode()).isEqualTo(HTTP_INTERNAL_ERROR);
184+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
185+
assertThat(capturedResponse.get()).isNotNull();
186+
assertThat(capturedResponse.get().status()).isEqualTo(HTTP_INTERNAL_ERROR);
187+
assertThat(capturedResponse.get().body()).isNull();
188+
}
189+
}
190+
191+
@Test
192+
void preRequestFailureSkipsHooks() throws Exception {
193+
List<String> log = new CopyOnWriteArrayList<>();
194+
195+
try (OpenApiServer server =
196+
baseBuilder()
197+
.handlers(Map.of(OK_OPERATION_ID, req -> Response.status(HTTP_NO_CONTENT)))
198+
.afterResponseHook((req, resp) -> log.add("fired"))
199+
.build()) {
200+
201+
HttpResponse<Void> resp =
202+
HttpClient.newHttpClient()
203+
.send(
204+
HttpRequest.newBuilder(uri(server, NOT_FOUND_PATH)).GET().build(),
205+
BodyHandlers.discarding());
206+
207+
assertThat(resp.statusCode()).isEqualTo(HTTP_NOT_FOUND);
208+
// Give the server-side thread a moment to complete any potential async work
209+
Thread.sleep(100);
210+
assertThat(log).isEmpty();
211+
}
212+
}
213+
214+
@Test
215+
void hookSeesScopedRequest() throws Exception {
216+
AtomicReference<Request> hookScopedRequest = new AtomicReference<>();
217+
CountDownLatch latch = new CountDownLatch(1);
218+
219+
try (OpenApiServer server =
220+
baseBuilder()
221+
.handlers(Map.of(OK_OPERATION_ID, req -> Response.status(HTTP_NO_CONTENT)))
222+
.afterResponseHook(
223+
(req, resp) -> {
224+
hookScopedRequest.set(DispatchHandler.CURRENT.get());
225+
latch.countDown();
226+
})
227+
.build()) {
228+
229+
HttpClient.newHttpClient()
230+
.send(
231+
HttpRequest.newBuilder(uri(server, OK_PATH)).GET().build(),
232+
BodyHandlers.discarding());
233+
234+
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
235+
assertThat(hookScopedRequest.get()).isNotNull();
236+
assertThat(hookScopedRequest.get().operationId()).isEqualTo(OK_OPERATION_ID);
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)