|
1 | 1 | package io.prerender; |
2 | 2 |
|
3 | | -import com.github.tomakehurst.wiremock.client.WireMock; |
4 | | -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; |
| 3 | +import com.github.tomakehurst.wiremock.http.Fault; |
5 | 4 | import com.github.tomakehurst.wiremock.junit5.WireMockExtension; |
6 | | -import jakarta.servlet.FilterChain; |
| 5 | +import jakarta.servlet.DispatcherType; |
| 6 | +import jakarta.servlet.http.HttpServlet; |
7 | 7 | import jakarta.servlet.http.HttpServletRequest; |
8 | 8 | 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; |
9 | 16 | import org.junit.jupiter.api.BeforeEach; |
10 | 17 | import org.junit.jupiter.api.Test; |
11 | | -import org.junit.jupiter.api.extension.ExtendWith; |
12 | 18 | import org.junit.jupiter.api.extension.RegisterExtension; |
13 | | -import org.mockito.Mock; |
14 | | -import org.mockito.junit.jupiter.MockitoExtension; |
15 | 19 |
|
16 | | -import java.io.PrintWriter; |
17 | | -import java.io.StringWriter; |
| 20 | +import java.io.IOException; |
| 21 | +import java.net.URI; |
18 | 22 | import java.net.http.HttpClient; |
| 23 | +import java.net.http.HttpRequest; |
| 24 | +import java.net.http.HttpResponse; |
| 25 | +import java.util.EnumSet; |
19 | 26 |
|
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; |
21 | 30 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; |
22 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; |
23 | | -import static org.mockito.ArgumentMatchers.any; |
24 | | -import static org.mockito.Mockito.*; |
25 | 32 |
|
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 | + */ |
27 | 40 | class PrerenderFilterTest { |
28 | 41 |
|
29 | 42 | private static final String BOT_UA = "Mozilla/5.0 (compatible; Googlebot/2.1)"; |
30 | 43 | private static final String BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"; |
31 | 44 | private static final String PRERENDERED_HTML = "<html><body>prerendered</body></html>"; |
| 45 | + private static final String ORIGINAL = "original"; |
32 | 46 |
|
33 | 47 | @RegisterExtension |
34 | 48 | static WireMockExtension wireMock = WireMockExtension.newInstance() |
35 | 49 | .options(wireMockConfig().dynamicPort()) |
36 | 50 | .build(); |
37 | 51 |
|
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 | + } |
41 | 73 |
|
42 | | - private StringWriter responseWriter; |
43 | | - private PrerenderFilter filter; |
| 74 | + @AfterAll |
| 75 | + static void stopJetty() throws Exception { |
| 76 | + if (jetty != null) jetty.stop(); |
| 77 | + } |
44 | 78 |
|
45 | 79 | @BeforeEach |
46 | | - void setUp() throws Exception { |
| 80 | + void resetStubs() { |
47 | 81 | 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()); |
52 | 94 | } |
53 | 95 |
|
54 | 96 | @Test |
55 | 97 | 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); |
63 | 99 |
|
64 | | - verify(chain).doFilter(request, response); |
65 | | - verify(response, never()).setStatus(anyInt()); |
| 100 | + assertEquals(200, res.statusCode()); |
| 101 | + assertEquals(ORIGINAL, res.body()); |
66 | 102 | } |
67 | 103 |
|
68 | 104 | @Test |
69 | 105 | void botRequest_receivesPrerenderedResponse() throws Exception { |
70 | 106 | wireMock.stubFor(get(anyUrl()) |
71 | 107 | .willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML))); |
72 | 108 |
|
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); |
82 | 110 |
|
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()); |
86 | 113 | } |
87 | 114 |
|
88 | 115 | @Test |
89 | 116 | 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); |
92 | 118 |
|
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()); |
97 | 121 | } |
98 | 122 |
|
99 | 123 | @Test |
100 | 124 | void escapedFragment_triggersPrerender() throws Exception { |
101 | 125 | wireMock.stubFor(get(anyUrl()) |
102 | 126 | .willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML))); |
103 | 127 |
|
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); |
112 | 129 |
|
113 | | - verify(response).setStatus(200); |
114 | | - verify(chain, never()).doFilter(any(), any()); |
| 130 | + assertEquals(200, res.statusCode()); |
| 131 | + assertEquals(PRERENDERED_HTML, res.body()); |
115 | 132 | } |
116 | 133 |
|
117 | 134 | @Test |
118 | 135 | void xBufferbot_triggersPrerender() throws Exception { |
119 | 136 | wireMock.stubFor(get(anyUrl()) |
120 | 137 | .willReturn(aResponse().withStatus(200).withBody(PRERENDERED_HTML))); |
121 | 138 |
|
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"); |
131 | 140 |
|
132 | | - verify(response).setStatus(200); |
133 | | - verify(chain, never()).doFilter(any(), any()); |
| 141 | + assertEquals(200, res.statusCode()); |
| 142 | + assertEquals(PRERENDERED_HTML, res.body()); |
134 | 143 | } |
135 | 144 |
|
136 | 145 | @Test |
137 | 146 | void postRequest_passesThrough() throws Exception { |
138 | | - when(request.getMethod()).thenReturn("POST"); |
| 147 | + HttpResponse<String> res = send("POST", "/", BOT_UA); |
139 | 148 |
|
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()); |
144 | 151 | } |
145 | 152 |
|
146 | 153 | @Test |
147 | 154 | void networkError_fallsBackToNormalResponse() throws Exception { |
148 | 155 | 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))); |
150 | 157 |
|
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); |
158 | 159 |
|
159 | | - filter.doFilter(request, response, chain); |
| 160 | + assertEquals(200, res.statusCode()); |
| 161 | + assertEquals(ORIGINAL, res.body()); |
| 162 | + } |
160 | 163 |
|
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 | + } |
162 | 171 | } |
163 | 172 | } |
0 commit comments