1 package de.juplo.httpresources;
2
3 import org.junit.jupiter.api.BeforeEach;
4 import org.junit.jupiter.api.Test;
5 import org.mockserver.integration.ClientAndServer;
6 import org.slf4j.Logger;
7 import org.slf4j.LoggerFactory;
8 import org.springframework.beans.factory.annotation.Autowired;
9 import org.springframework.beans.factory.annotation.Value;
10 import org.springframework.boot.autoconfigure.SpringBootApplication;
11 import org.springframework.boot.test.context.SpringBootTest;
12 import org.springframework.cache.Cache;
13 import org.springframework.core.io.Resource;
14 import org.springframework.test.web.servlet.MockMvc;
15 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
16 import org.springframework.util.StreamUtils;
17 import org.springframework.web.context.WebApplicationContext;
18 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
19 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
20
21 import java.net.URI;
22 import java.nio.charset.Charset;
23 import java.time.Duration;
24
25 import static de.juplo.httpresources.TestUtil.CONTENT;
26 import static de.juplo.httpresources.TestUtil.read;
27 import static org.junit.jupiter.api.Assertions.*;
28 import static org.mockserver.model.HttpRequest.request;
29 import static org.mockserver.verify.VerificationTimes.exactly;
30 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
31 import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
32 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
33 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
34
35
36
37
38
39
40 @SpringBootTest({
41 "juplo.http-resources.protocol-resolver.enabled=true",
42 "juplo.http-resources.resolver.enabled=false",
43 "juplo.http-resources.resolver.exclusion-patterns=**.txt"
44 })
45 public class HttpResourceProtocolResolverIT extends IntegrationTestBase
46 {
47 private final static Logger LOG =
48 LoggerFactory.getLogger(HttpResourceProtocolResolverIT.class);
49
50
51 @Autowired
52 HttpResources resources;
53 @Autowired
54 Cache cache;
55 @Autowired
56 WebApplicationContext context;
57
58 @Value("classpath:remote/remote.html")
59 Resource remote;
60 @Value("classpath:remote/modified.html")
61 Resource modified;
62
63 MockMvc mvc;
64
65
66 @BeforeEach
67 public void setUp()
68 {
69 cache.clear();
70 mvc = MockMvcBuilders
71 .webAppContextSetup(context)
72 .alwaysDo(print())
73 .build();
74 }
75
76
77
78
79
80
81
82
83
84
85
86 @Test
87 public void testResourceHandling() throws Exception
88 {
89 LOG.info("<-- start of test-case");
90
91 mvc
92 .perform(get(URI.create("http://test/foo")))
93 .andExpect(status().isOk())
94 .andExpect(content().contentType("application/octet-stream"))
95 .andExpect(content().string("FOO\n"));
96 mvc
97 .perform(get(URI.create("http://test/bar")))
98 .andExpect(status().isOk())
99 .andExpect(content().contentType("application/octet-stream"))
100 .andExpect(content().string("BAR\n"));
101 mvc
102 .perform(get(URI.create("http://test/hello")))
103 .andExpect(status().isOk())
104 .andExpect(content().contentType("application/octet-stream"))
105 .andExpect(content().bytes(CONTENT));
106 mvc
107 .perform(get(URI.create("http://test/remote.html")))
108 .andExpect(status().isOk())
109 .andExpect(content().contentType("text/html"))
110 .andExpect(content().bytes(StreamUtils.copyToByteArray(remote.getInputStream())));
111 mvc
112 .perform(get(URI.create("http://test/foobar")))
113 .andExpect(status().isOk())
114 .andExpect(content().contentType("application/octet-stream"))
115 .andExpect(content().string("FOOBAR\n"));
116 mvc
117 .perform(get(URI.create("http://test/hello")))
118 .andExpect(status().isOk())
119 .andExpect(content().contentType("application/octet-stream"))
120 .andExpect(content().bytes(CONTENT));
121 mvc
122 .perform(get(URI.create("http://test/hello")))
123 .andExpect(status().isOk())
124 .andExpect(content().contentType("application/octet-stream"))
125 .andExpect(content().bytes(CONTENT));
126 mvc
127 .perform(get(URI.create("http://test/hello")))
128 .andExpect(status().isOk())
129 .andExpect(content().contentType("application/octet-stream"))
130 .andExpect(content().bytes(CONTENT));
131 mvc
132 .perform(get(URI.create("http://test/hallo.txt")))
133 .andExpect(status().isOk())
134 .andExpect(content().contentType("text/plain"))
135 .andExpect(content().string("welt\n"));
136
137 server.verify(FETCH("/foo"), exactly(0));
138 server.verify(FETCH("/bar"), exactly(0));
139 server.verify(FETCH("/hello"), exactly(1));
140 server.verify(FETCH("/hallo.txt"), exactly(1));
141 server.verify(FETCH("/remote.html"), exactly(1));
142 server.verify(FETCH("/foobar"), exactly(2));
143 }
144
145 @Test
146 public void testFetchExistent() throws Exception
147 {
148 LOG.info("<-- Start of test-case");
149
150 getRemoteHtml();
151
152 server.verify(FETCH("/remote.html"), exactly(1));
153 }
154
155 @Test
156 public void testCachingOfExisting() throws Exception
157 {
158 LOG.info("<-- Start of test-case");
159
160 getRemoteHtml();
161 getRemoteHtml();
162 getRemoteHtml();
163 getRemoteHtml();
164 getRemoteHtml();
165 getRemoteHtml();
166 getRemoteHtml();
167 getRemoteHtml();
168
169 server.verify(FETCH("/remote.html"), exactly(1));
170 }
171
172 private void getRemoteHtml() throws Exception
173 {
174 Resource resource = context.getResource(address("/remote.html"));
175 assertNotNull(resource);
176 assertTrue(resource.exists());
177 String expected = StreamUtils.copyToString(remote.getInputStream(), Charset.forName("UTF-8"));
178 String content = StreamUtils.copyToString(resource.getInputStream(), Charset.forName("UTF-8"));
179 assertEquals(expected, content);
180 }
181
182 @Test
183 public void testFetchNonExistent() throws Exception
184 {
185 LOG.info("<-- Start of test-case");
186
187 getNonExistingHtml();
188
189 server.verify(FETCH("/peter.html"), exactly(2));
190 }
191
192 @Test
193 public void testCachingOfNonExistent() throws Exception
194 {
195 LOG.info("<-- Start of test-case");
196
197 getNonExistingHtml();
198 getNonExistingHtml();
199 getNonExistingHtml();
200 getNonExistingHtml();
201 getNonExistingHtml();
202 getNonExistingHtml();
203 getNonExistingHtml();
204
205
206 server.verify(FETCH("/peter.html"), exactly(14));
207 }
208
209 private void getNonExistingHtml() throws Exception
210 {
211 Resource resource = context.getResource(address("/peter.html"));
212 assertNotNull(resource);
213 assertFalse(resource.exists());
214 }
215
216 @Test
217 public void testModifiedResource() throws Exception
218 {
219 LOG.info("<-- Start of test-case");
220
221 mvc
222 .perform(get(URI.create("http://test/remote.html")))
223 .andExpect(status().isOk())
224 .andExpect(content().string(read(remote)));
225
226 CLOCK.timetravel(Duration.ofSeconds(10));
227 server.when(FETCH("/remote.html")).forward(NGINX("/modified.html"));
228
229 mvc
230 .perform(get(URI.create("http://test/remote.html")))
231 .andExpect(status().isOk())
232 .andExpect(content().string(read(modified)));
233
234 server.verify(FETCH("/remote.html"), exactly(2));
235 server.verify(
236 request()
237 .withPath("/remote.html")
238 .withHeader("If-Modified-Since")
239 .withHeader("If-None-Match"),
240 exactly(1));
241 }
242
243
244 @SpringBootApplication
245 public static class Application implements WebMvcConfigurer
246 {
247 @Autowired
248 ClientAndServer server;
249
250 @Override
251 public void addResourceHandlers(ResourceHandlerRegistry registry)
252 {
253 LOG.info(
254 "{} resource-handler for static location {}",
255 registry.hasMappingForPattern("/**") ? "Overwriting" : "Setting",
256 "/**"
257 );
258 registry
259 .addResourceHandler("/**")
260 .addResourceLocations(
261 "classpath:/static/",
262 "classpath:/public/",
263 "http://localhost:" + server.getLocalPort(),
264 "classpath:/fallback/")
265 .resourceChain(false);
266 }
267 }
268 }