1 package de.juplo.httpresources;
2
3 import org.apache.http.entity.StringEntity;
4 import org.junit.jupiter.api.BeforeEach;
5 import org.junit.jupiter.api.Test;
6 import org.mockserver.integration.ClientAndServer;
7 import org.slf4j.Logger;
8 import org.slf4j.LoggerFactory;
9 import org.springframework.beans.factory.annotation.Autowired;
10 import org.springframework.boot.autoconfigure.SpringBootApplication;
11 import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
12 import org.springframework.boot.test.context.SpringBootTest;
13 import org.springframework.cache.Cache;
14 import org.springframework.context.ApplicationContext;
15 import org.springframework.context.annotation.Bean;
16 import org.springframework.core.io.Resource;
17 import org.springframework.stereotype.Controller;
18 import org.springframework.test.web.servlet.MockMvc;
19 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
20 import org.springframework.ui.Model;
21 import org.springframework.web.bind.annotation.RequestMapping;
22 import org.springframework.web.bind.annotation.RequestParam;
23 import org.springframework.web.context.WebApplicationContext;
24 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
25 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
26 import org.springframework.web.util.NestedServletException;
27 import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
28
29 import java.io.IOException;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.nio.file.Files;
33 import java.nio.file.Paths;
34 import java.time.Duration;
35 import java.util.Scanner;
36 import java.util.stream.Collectors;
37
38 import static org.junit.jupiter.api.Assertions.assertThrows;
39 import static org.mockserver.model.HttpRequest.request;
40 import static org.mockserver.verify.VerificationTimes.exactly;
41 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
42 import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
43 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
44 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 @SpringBootTest({
62 "juplo.http-resources.protocol-resolver.enabled=true",
63 "spring.thymeleaf.cache=true" })
64 public class ThymeleafWithHttpResourceProtocolResolverIT extends IntegrationTestBase
65 {
66 private static final Logger LOG =
67 LoggerFactory.getLogger(ThymeleafWithHttpResourceProtocolResolverIT.class);
68
69
70 @Autowired
71 HttpResources resources;
72 @Autowired
73 Cache cache;
74 @Autowired
75 WebApplicationContext context;
76
77 MockMvc mvc;
78
79
80 @BeforeEach
81 public void setUp()
82 {
83 cache.clear();
84 mvc = MockMvcBuilders
85 .webAppContextSetup(context)
86 .alwaysDo(print())
87 .build();
88 }
89
90
91 @Test
92 public void testRenderLocalTemplate() throws Exception
93 {
94 LOG.info("<-- start of test-case");
95
96 assertThrows(
97 NestedServletException.class,
98 () -> mvc.perform(get(URI.create("http://test/controller.html?template=local"))));
99
100 server.verify(FETCH("/local.html"), exactly(2));
101 }
102
103 @Test
104 public void testRenderRemoteTemplate() throws Exception
105 {
106 LOG.info("<-- start of test-case");
107
108 mvc
109 .perform(get(URI.create("http://test/controller.html?template=remote")))
110 .andExpect(status().isOk())
111 .andExpect(content().string(read("/rendered/remote.html")));
112
113 server.verify(FETCH("/remote.html"), exactly(1));
114 }
115
116 @Test
117 public void testRenderCachedRemoteTemplate() throws Exception
118 {
119 LOG.info("<-- start of test-case");
120
121 mvc
122 .perform(get(URI.create("http://test/controller.html?template=remote")))
123 .andExpect(status().isOk())
124 .andExpect(content().string(read("/rendered/remote.html")));
125
126 mvc
127 .perform(get(URI.create("http://test/controller.html?template=remote")))
128 .andExpect(status().isOk())
129 .andExpect(content().string(read("/rendered/remote.html")));
130
131 mvc
132 .perform(get(URI.create("http://test/controller.html?template=remote")))
133 .andExpect(status().isOk())
134 .andExpect(content().string(read("/rendered/remote.html")));
135
136 mvc
137 .perform(get(URI.create("http://test/controller.html?template=remote")))
138 .andExpect(status().isOk())
139 .andExpect(content().string(read("/rendered/remote.html")));
140
141 server.verify(FETCH("/remote.html"), exactly(1));
142 }
143
144 @Test
145 public void testRenderModifiedRemoteTemplate() throws Exception
146 {
147 LOG.info("<-- start of test-case");
148
149 mvc
150 .perform(get(URI.create("http://test/controller.html?template=remote")))
151 .andExpect(status().isOk())
152 .andExpect(content().string(read("/rendered/remote.html")));
153
154 CLOCK.timetravel(Duration.ofSeconds(10));
155 server.when(FETCH("/remote.html")).forward(NGINX("/modified.html"));
156
157 mvc
158 .perform(get(URI.create("http://test/controller.html?template=remote")))
159 .andExpect(status().isOk())
160 .andExpect(content().string(read("/rendered/modified.html")));
161
162 server.verify(FETCH("/remote.html"), exactly(2));
163 server.verify(
164 request()
165 .withPath("/remote.html")
166 .withHeader("If-Modified-Since")
167 .withHeader("If-None-Match"),
168 exactly(1));
169 }
170
171
172 @Controller
173 public static class TestController
174 {
175
176 @RequestMapping("/controller.html")
177 public String controller(
178 @RequestParam String template,
179 Model model
180 )
181 {
182 model.addAttribute("template", template);
183 return template;
184 }
185
186 }
187
188
189 static StringEntity body(String resource) throws URISyntaxException, IOException
190 {
191 return new StringEntity(read(resource));
192 }
193
194 static String read(String resource) throws URISyntaxException, IOException
195 {
196 URI uri = ThymeleafWithHttpResourceProtocolResolverIT.class.getResource(resource).toURI();
197 return Files.readAllLines(Paths.get(uri)).stream().collect(Collectors.joining("\n"));
198 }
199
200 static String read(Resource resource) throws URISyntaxException, IOException
201 {
202 Scanner s = new Scanner(resource.getInputStream()).useDelimiter("\\A");
203 return s.hasNext() ? s.next() : "";
204 }
205
206
207 @SpringBootApplication
208 public static class Application implements WebMvcConfigurer
209 {
210 @Autowired
211 ClientAndServer server;
212
213
214
215
216
217 @Bean
218 public SpringResourceTemplateResolver defaultTemplateResolver(
219 ThymeleafProperties properties,
220 ApplicationContext applicationContext,
221 ClientAndServer server)
222 {
223 SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
224 resolver.setApplicationContext(applicationContext);
225 resolver.setPrefix("http://localhost:" + server.getLocalPort() + "/"); // << Cannot be passed in via properties
226 resolver.setSuffix(properties.getSuffix());
227 resolver.setTemplateMode(properties.getMode());
228 if (properties.getEncoding() != null)
229 {
230 resolver.setCharacterEncoding(properties.getEncoding().name());
231 }
232 resolver.setCacheable(false);
233 Integer order = properties.getTemplateResolverOrder();
234 if (order != null)
235 {
236 resolver.setOrder(order);
237 }
238 resolver.setCheckExistence(properties.isCheckTemplate());
239 return resolver;
240 }
241
242 @Bean
243 public TestController testController()
244 {
245 return new TestController();
246 }
247
248 @Override
249 public void addResourceHandlers(ResourceHandlerRegistry registry)
250 {
251 LOG.info(
252 "{} resource-handler for static location {}",
253 registry.hasMappingForPattern("/**") ? "Overwriting" : "Setting",
254 "/**"
255 );
256 registry
257 .addResourceHandler("/**")
258 .addResourceLocations(
259 "classpath:/static/",
260 "classpath:/public/",
261 "http://localhost:" + server.getLocalPort(),
262 "classpath:/fallback/")
263 .resourceChain(false);
264 }
265 }
266 }