View Javadoc
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   * Mostly identically with {@link HttpResourceResolverIT}.
37   * Differences:Test explicit lookup with full address through context and has
38   * no exclusion/inclusion.
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" // << Not used here!
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     * Olthough the {@HttpResourceResolver} is not enabled in this test-scenario,
79     * this works nearly, identically to the same test there, because all lookups
80     * for resources in the resource-chain for static http-resources are executed
81     * through the application-context, where the
82     * {@link HttpResourceProtocolResolver} is registerd.
83     * Only exclusion is not working, since this is only implemented in the
84     * {@HttpResourceResolver}.
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     // Remote-requests answered with 404 are repeated
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 }