Skip to content

Commit e206679

Browse files
Marcinclaude
andcommitted
test: drive filter inside embedded Jetty instead of Mockito
Mockito mocks on HttpServletRequest/Response don't exercise the real servlet container: filter chain wiring, URL/query construction, status and header propagation, static-asset pass-through are all simulated. Boot Jetty on a free port, register PrerenderFilter via FilterHolder with init params, mount a tiny servlet for the pass-through case, and hit the whole thing with a real HttpClient. Upstream Prerender service remains faked with the already-present WireMock. Jetty pinned to 11.0.20 to match WireMock 3.5.4's transitive version and avoid a classpath conflict with Jetty 12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 56cb227 commit e206679

2 files changed

Lines changed: 100 additions & 85 deletions

File tree

‎pom.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
<version>3.5.4</version>
6565
<scope>test</scope>
6666
</dependency>
67+
<dependency>
68+
<groupId>org.eclipse.jetty</groupId>
69+
<artifactId>jetty-servlet</artifactId>
70+
<version>11.0.20</version>
71+
<scope>test</scope>
72+
</dependency>
6773
<dependency>
6874
<groupId>com.fasterxml.jackson.core</groupId>
6975
<artifactId>jackson-databind</artifactId>
Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,172 @@
11
package io.prerender;
22

3-
import com.github.tomakehurst.wiremock.client.WireMock;
4-
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
3+
import com.github.tomakehurst.wiremock.http.Fault;
54
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
6-
import jakarta.servlet.FilterChain;
5+
import jakarta.servlet.DispatcherType;
6+
import jakarta.servlet.http.HttpServlet;
77
import jakarta.servlet.http.HttpServletRequest;
88
import jakarta.servlet.http.HttpServletResponse;
9+
import org.eclipse.jetty.server.Server;
10+
import org.eclipse.jetty.server.ServerConnector;
11+
import org.eclipse.jetty.servlet.FilterHolder;
12+
import org.eclipse.jetty.servlet.ServletContextHandler;
13+
import org.eclipse.jetty.servlet.ServletHolder;
14+
import org.junit.jupiter.api.AfterAll;
15+
import org.junit.jupiter.api.BeforeAll;
916
import org.junit.jupiter.api.BeforeEach;
1017
import org.junit.jupiter.api.Test;
11-
import org.junit.jupiter.api.extension.ExtendWith;
1218
import org.junit.jupiter.api.extension.RegisterExtension;
13-
import org.mockito.Mock;
14-
import org.mockito.junit.jupiter.MockitoExtension;
1519

16-
import java.io.PrintWriter;
17-
import java.io.StringWriter;
20+
import java.io.IOException;
21+
import java.net.URI;
1822
import java.net.http.HttpClient;
23+
import java.net.http.HttpRequest;
24+
import java.net.http.HttpResponse;
25+
import java.util.EnumSet;
1926

20-
import static com.github.tomakehurst.wiremock.client.WireMock.*;
27+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
28+
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
29+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
2130
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
2231
import static org.junit.jupiter.api.Assertions.assertEquals;
23-
import static org.mockito.ArgumentMatchers.any;
24-
import static org.mockito.Mockito.*;
2532

26-
@ExtendWith(MockitoExtension.class)
33+
/**
34+
* Drives the filter inside a real servlet container (embedded Jetty), hit by a real
35+
* HTTP client. The upstream Prerender service is faked with WireMock. Catches
36+
* container-level behaviour (filter chain wiring, request URL/query, status and
37+
* header propagation, static-asset pass-through) that Mockito on servlet objects
38+
* would miss.
39+
*/
2740
class PrerenderFilterTest {
2841

2942
private static final String BOT_UA = "Mozilla/5.0 (compatible; Googlebot/2.1)";
3043
private static final String BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
3144
private static final String PRERENDERED_HTML = "<html><body>prerendered</body></html>";
45+
private static final String ORIGINAL = "original";
3246

3347
@RegisterExtension
3448
static WireMockExtension wireMock = WireMockExtension.newInstance()
3549
.options(wireMockConfig().dynamicPort())
3650
.build();
3751

38-
@Mock private HttpServletRequest request;
39-
@Mock private HttpServletResponse response;
40-
@Mock private FilterChain chain;
52+
private static Server jetty;
53+
private static String baseUrl;
54+
private static final HttpClient httpClient = HttpClient.newHttpClient();
55+
56+
@BeforeAll
57+
static void startJetty() throws Exception {
58+
jetty = new Server(0);
59+
ServletContextHandler context = new ServletContextHandler();
60+
context.setContextPath("/");
61+
62+
FilterHolder filter = new FilterHolder(new PrerenderFilter());
63+
filter.setInitParameter("prerenderToken", "test-token");
64+
filter.setInitParameter("prerenderServiceUrl", wireMock.baseUrl());
65+
context.addFilter(filter, "/*", EnumSet.of(DispatcherType.REQUEST));
66+
context.addServlet(new ServletHolder(new OriginalServlet()), "/*");
67+
68+
jetty.setHandler(context);
69+
jetty.start();
70+
int port = ((ServerConnector) jetty.getConnectors()[0]).getLocalPort();
71+
baseUrl = "http://127.0.0.1:" + port;
72+
}
4173

42-
private StringWriter responseWriter;
43-
private PrerenderFilter filter;
74+
@AfterAll
75+
static void stopJetty() throws Exception {
76+
if (jetty != null) jetty.stop();
77+
}
4478

4579
@BeforeEach
46-
void setUp() throws Exception {
80+
void resetStubs() {
4781
wireMock.resetAll();
48-
responseWriter = new StringWriter();
49-
lenient().when(response.getWriter()).thenReturn(new PrintWriter(responseWriter));
50-
PrerenderConfig config = new PrerenderConfig(null, "http://localhost:" + wireMock.getPort());
51-
filter = new PrerenderFilter(HttpClient.newHttpClient(), config);
82+
}
83+
84+
private HttpResponse<String> send(String method, String path, String userAgent, String... extraHeaders) throws Exception {
85+
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(baseUrl + path))
86+
.header("User-Agent", userAgent);
87+
for (int i = 0; i + 1 < extraHeaders.length; i += 2) {
88+
b.header(extraHeaders[i], extraHeaders[i + 1]);
89+
}
90+
HttpRequest req = "POST".equals(method)
91+
? b.POST(HttpRequest.BodyPublishers.noBody()).build()
92+
: b.GET().build();
93+
return httpClient.send(req, HttpResponse.BodyHandlers.ofString());
5294
}
5395

5496
@Test
5597
void browserRequest_passesThrough() throws Exception {
56-
when(request.getMethod()).thenReturn("GET");
57-
when(request.getRequestURI()).thenReturn("/");
58-
when(request.getParameter("_escaped_fragment_")).thenReturn(null);
59-
when(request.getHeader("X-Bufferbot")).thenReturn(null);
60-
when(request.getHeader("User-Agent")).thenReturn(BROWSER_UA);
61-
62-
filter.doFilter(request, response, chain);
98+
HttpResponse<String> res = send("GET", "/", BROWSER_UA);
6399

64-
verify(chain).doFilter(request, response);
65-
verify(response, never()).setStatus(anyInt());
100+
assertEquals(200, res.statusCode());
101+
assertEquals(ORIGINAL, res.body());
66102
}
67103

68104
@Test
69105
void botRequest_receivesPrerenderedResponse() throws Exception {
70106
wireMock.stubFor(get(anyUrl())
71107
.willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML)));
72108

73-
when(request.getMethod()).thenReturn("GET");
74-
when(request.getRequestURI()).thenReturn("/");
75-
when(request.getParameter("_escaped_fragment_")).thenReturn(null);
76-
when(request.getHeader("X-Bufferbot")).thenReturn(null);
77-
when(request.getHeader("User-Agent")).thenReturn(BOT_UA);
78-
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/"));
79-
when(request.getQueryString()).thenReturn(null);
80-
81-
filter.doFilter(request, response, chain);
109+
HttpResponse<String> res = send("GET", "/about", BOT_UA);
82110

83-
verify(response).setStatus(200);
84-
verify(chain, never()).doFilter(any(), any());
85-
assertEquals(PRERENDERED_HTML, responseWriter.toString());
111+
assertEquals(200, res.statusCode());
112+
assertEquals(PRERENDERED_HTML, res.body());
86113
}
87114

88115
@Test
89116
void botRequest_staticAsset_passesThrough() throws Exception {
90-
when(request.getMethod()).thenReturn("GET");
91-
when(request.getRequestURI()).thenReturn("/styles.css");
117+
HttpResponse<String> res = send("GET", "/styles.css", BOT_UA);
92118

93-
filter.doFilter(request, response, chain);
94-
95-
verify(chain).doFilter(request, response);
96-
verify(response, never()).setStatus(anyInt());
119+
assertEquals(200, res.statusCode());
120+
assertEquals(ORIGINAL, res.body());
97121
}
98122

99123
@Test
100124
void escapedFragment_triggersPrerender() throws Exception {
101125
wireMock.stubFor(get(anyUrl())
102126
.willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML)));
103127

104-
when(request.getMethod()).thenReturn("GET");
105-
when(request.getRequestURI()).thenReturn("/");
106-
when(request.getParameter("_escaped_fragment_")).thenReturn("");
107-
when(request.getHeader("User-Agent")).thenReturn(BROWSER_UA);
108-
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/"));
109-
when(request.getQueryString()).thenReturn("_escaped_fragment_=");
110-
111-
filter.doFilter(request, response, chain);
128+
HttpResponse<String> res = send("GET", "/?_escaped_fragment_=", BROWSER_UA);
112129

113-
verify(response).setStatus(200);
114-
verify(chain, never()).doFilter(any(), any());
130+
assertEquals(200, res.statusCode());
131+
assertEquals(PRERENDERED_HTML, res.body());
115132
}
116133

117134
@Test
118135
void xBufferbot_triggersPrerender() throws Exception {
119136
wireMock.stubFor(get(anyUrl())
120137
.willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML)));
121138

122-
when(request.getMethod()).thenReturn("GET");
123-
when(request.getRequestURI()).thenReturn("/");
124-
when(request.getParameter("_escaped_fragment_")).thenReturn(null);
125-
when(request.getHeader("X-Bufferbot")).thenReturn("true");
126-
when(request.getHeader("User-Agent")).thenReturn(BROWSER_UA);
127-
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/"));
128-
when(request.getQueryString()).thenReturn(null);
129-
130-
filter.doFilter(request, response, chain);
139+
HttpResponse<String> res = send("GET", "/", BROWSER_UA, "X-Bufferbot", "true");
131140

132-
verify(response).setStatus(200);
133-
verify(chain, never()).doFilter(any(), any());
141+
assertEquals(200, res.statusCode());
142+
assertEquals(PRERENDERED_HTML, res.body());
134143
}
135144

136145
@Test
137146
void postRequest_passesThrough() throws Exception {
138-
when(request.getMethod()).thenReturn("POST");
147+
HttpResponse<String> res = send("POST", "/", BOT_UA);
139148

140-
filter.doFilter(request, response, chain);
141-
142-
verify(chain).doFilter(request, response);
143-
verify(response, never()).setStatus(anyInt());
149+
assertEquals(200, res.statusCode());
150+
assertEquals(ORIGINAL, res.body());
144151
}
145152

146153
@Test
147154
void networkError_fallsBackToNormalResponse() throws Exception {
148155
wireMock.stubFor(get(anyUrl())
149-
.willReturn(aResponse().withFault(com.github.tomakehurst.wiremock.http.Fault.CONNECTION_RESET_BY_PEER)));
156+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
150157

151-
when(request.getMethod()).thenReturn("GET");
152-
when(request.getRequestURI()).thenReturn("/");
153-
when(request.getParameter("_escaped_fragment_")).thenReturn(null);
154-
when(request.getHeader("X-Bufferbot")).thenReturn(null);
155-
when(request.getHeader("User-Agent")).thenReturn(BOT_UA);
156-
when(request.getRequestURL()).thenReturn(new StringBuffer("http://example.com/"));
157-
when(request.getQueryString()).thenReturn(null);
158+
HttpResponse<String> res = send("GET", "/", BOT_UA);
158159

159-
filter.doFilter(request, response, chain);
160+
assertEquals(200, res.statusCode());
161+
assertEquals(ORIGINAL, res.body());
162+
}
160163

161-
verify(chain).doFilter(request, response);
164+
public static class OriginalServlet extends HttpServlet {
165+
@Override
166+
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
167+
resp.setStatus(200);
168+
resp.setContentType("text/plain");
169+
resp.getWriter().write(ORIGINAL);
170+
}
162171
}
163172
}

0 commit comments

Comments
 (0)