/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ServerWebExchange;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Unit tests for {@link ResourceWebHandler}.
*
* @author Rossen Stoyanchev
*/
public class ResourceWebHandlerTests {
private static final Duration TIMEOUT = Duration.ofSeconds(1);
private ResourceWebHandler handler;
private DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Before
public void setup() throws Exception {
List<Resource> paths = new ArrayList<>(2);
paths.add(new ClassPathResource("test/", getClass()));
paths.add(new ClassPathResource("testalternatepath/", getClass()));
paths.add(new ClassPathResource("META-INF/resources/webjars/"));
this.handler = new ResourceWebHandler();
this.handler.setLocations(paths);
this.handler.setCacheControl(CacheControl.maxAge(3600, TimeUnit.SECONDS));
this.handler.afterPropertiesSet();
this.handler.afterSingletonsInstantiated();
}
@Test
public void getResource() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
HttpHeaders headers = exchange.getResponse().getHeaders();
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
assertEquals(17, headers.getContentLength());
assertEquals("max-age=3600", headers.getCacheControl());
assertTrue(headers.containsKey("Last-Modified"));
assertEquals(headers.getLastModified() / 1000, resourceLastModifiedDate("test/foo.css") / 1000);
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
assertEquals(1, headers.get("Accept-Ranges").size());
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test
public void getResourceHttpHeader() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.head("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertNull(exchange.getResponse().getStatusCode());
HttpHeaders headers = exchange.getResponse().getHeaders();
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
assertEquals(17, headers.getContentLength());
assertEquals("max-age=3600", headers.getCacheControl());
assertTrue(headers.containsKey("Last-Modified"));
assertEquals(headers.getLastModified() / 1000, resourceLastModifiedDate("test/foo.css") / 1000);
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
assertEquals(1, headers.get("Accept-Ranges").size());
StepVerifier.create(exchange.getResponse().getBody())
.expectErrorMatches(ex -> ex.getMessage().startsWith("The body is not set."))
.verify();
}
@Test
public void getResourceHttpOptions() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.options("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertNull(exchange.getResponse().getStatusCode());
assertEquals("GET,HEAD,OPTIONS", exchange.getResponse().getHeaders().getFirst("Allow"));
}
@Test
public void getResourceNoCache() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.setCacheControl(CacheControl.noStore());
this.handler.handle(exchange).block(TIMEOUT);
MockServerHttpResponse response = exchange.getResponse();
assertEquals("no-store", response.getHeaders().getCacheControl());
assertTrue(response.getHeaders().containsKey("Last-Modified"));
assertEquals(response.getHeaders().getLastModified() / 1000, resourceLastModifiedDate("test/foo.css") / 1000);
assertEquals("bytes", response.getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, response.getHeaders().get("Accept-Ranges").size());
}
@Test
public void getVersionedResource() throws Exception {
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.addFixedVersionStrategy("versionString", "/**");
this.handler.setResourceResolvers(Arrays.asList(versionResolver, new PathResourceResolver()));
this.handler.afterPropertiesSet();
this.handler.afterSingletonsInstantiated();
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "versionString/foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals("\"versionString\"", exchange.getResponse().getHeaders().getETag());
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
}
@Test
public void getResourceWithHtmlMediaType() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.html");
this.handler.handle(exchange).block(TIMEOUT);
HttpHeaders headers = exchange.getResponse().getHeaders();
assertEquals(MediaType.TEXT_HTML, headers.getContentType());
assertEquals("max-age=3600", headers.getCacheControl());
assertTrue(headers.containsKey("Last-Modified"));
assertEquals(headers.getLastModified() / 1000, resourceLastModifiedDate("test/foo.html") / 1000);
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
assertEquals(1, headers.get("Accept-Ranges").size());
}
@Test
public void getResourceFromAlternatePath() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "baz.css");
this.handler.handle(exchange).block(TIMEOUT);
HttpHeaders headers = exchange.getResponse().getHeaders();
assertEquals(MediaType.parseMediaType("text/css"), headers.getContentType());
assertEquals(17, headers.getContentLength());
assertEquals("max-age=3600", headers.getCacheControl());
assertTrue(headers.containsKey("Last-Modified"));
assertEquals(headers.getLastModified() / 1000, resourceLastModifiedDate("testalternatepath/baz.css") / 1000);
assertEquals("bytes", headers.getFirst("Accept-Ranges"));
assertEquals(1, headers.get("Accept-Ranges").size());
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test
public void getResourceFromSubDirectory() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "js/foo.js");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(MediaType.parseMediaType("application/javascript"), exchange.getResponse().getHeaders().getContentType());
assertResponseBody(exchange, "function foo() { console.log(\"hello world\"); }");
}
@Test
public void getResourceFromSubDirectoryOfAlternatePath() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "js/baz.js");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(MediaType.parseMediaType("application/javascript"), exchange.getResponse().getHeaders().getContentType());
assertResponseBody(exchange, "function foo() { console.log(\"hello world\"); }");
}
@Test // SPR-13658
public void getResourceWithRegisteredMediaType() throws Exception {
CompositeContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder()
.mediaType("css", new MediaType("foo", "bar"))
.build();
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(paths);
handler.setContentTypeResolver(contentTypeResolver);
handler.afterPropertiesSet();
handler.afterSingletonsInstantiated();
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
handler.handle(exchange).block(TIMEOUT);
assertEquals(MediaType.parseMediaType("foo/bar"), exchange.getResponse().getHeaders().getContentType());
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test // SPR-14577
public void getMediaTypeWithFavorPathExtensionOff() throws Exception {
CompositeContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder()
.favorPathExtension(false)
.build();
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(paths);
handler.setContentTypeResolver(contentTypeResolver);
handler.afterPropertiesSet();
handler.afterSingletonsInstantiated();
MockServerWebExchange exchange = MockServerHttpRequest.get("")
.header("Accept", "application/json,text/plain,*/*").toExchange();
setPathWithinHandlerMapping(exchange, "foo.html");
handler.handle(exchange).block(TIMEOUT);
assertEquals(MediaType.TEXT_HTML, exchange.getResponse().getHeaders().getContentType());
}
@Test
public void invalidPath() throws Exception {
for (HttpMethod method : HttpMethod.values()) {
testInvalidPath(method);
}
}
private void testInvalidPath(HttpMethod httpMethod) throws Exception {
Resource location = new ClassPathResource("test/", getClass());
this.handler.setLocations(Collections.singletonList(location));
testInvalidPath(httpMethod, "../testsecret/secret.txt", location);
testInvalidPath(httpMethod, "test/../../testsecret/secret.txt", location);
testInvalidPath(httpMethod, ":/../../testsecret/secret.txt", location);
location = new UrlResource(getClass().getResource("./test/"));
this.handler.setLocations(Collections.singletonList(location));
Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt"));
String secretPath = secretResource.getURL().getPath();
testInvalidPath(httpMethod, "file:" + secretPath, location);
testInvalidPath(httpMethod, "/file:" + secretPath, location);
testInvalidPath(httpMethod, "url:" + secretPath, location);
testInvalidPath(httpMethod, "/url:" + secretPath, location);
testInvalidPath(httpMethod, "////../.." + secretPath, location);
testInvalidPath(httpMethod, "/%2E%2E/testsecret/secret.txt", location);
testInvalidPath(httpMethod, "url:" + secretPath, location);
// The following tests fail with a MalformedURLException on Windows
// testInvalidPath(location, "/" + secretPath);
// testInvalidPath(location, "/ " + secretPath);
}
private void testInvalidPath(HttpMethod httpMethod, String requestPath, Resource location) throws Exception {
ServerWebExchange exchange = MockServerHttpRequest.method(httpMethod, "").toExchange();
setPathWithinHandlerMapping(exchange, requestPath);
this.handler.handle(exchange).block(TIMEOUT);
if (!location.createRelative(requestPath).exists() && !requestPath.contains(":")) {
fail(requestPath + " doesn't actually exist as a relative path");
}
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
}
@Test
public void ignoreInvalidEscapeSequence() throws Exception {
ServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "/%foo%/bar.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
}
@Test
public void processPath() throws Exception {
assertSame("/foo/bar", this.handler.processPath("/foo/bar"));
assertSame("foo/bar", this.handler.processPath("foo/bar"));
// leading whitespace control characters (00-1F)
assertEquals("/foo/bar", this.handler.processPath(" /foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 1 + "/foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 31 + "/foo/bar"));
assertEquals("foo/bar", this.handler.processPath(" foo/bar"));
assertEquals("foo/bar", this.handler.processPath((char) 31 + "foo/bar"));
// leading control character 0x7F (DEL)
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
// leading control and '/' characters
assertEquals("/foo/bar", this.handler.processPath(" / foo/bar"));
assertEquals("/foo/bar", this.handler.processPath(" / / foo/bar"));
assertEquals("/foo/bar", this.handler.processPath(" // /// //// foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 1 + " / " + (char) 127 + " // foo/bar"));
// root or empty path
assertEquals("", this.handler.processPath(" "));
assertEquals("/", this.handler.processPath("/"));
assertEquals("/", this.handler.processPath("///"));
assertEquals("/", this.handler.processPath("/ / / "));
}
@Test
public void initAllowedLocations() throws Exception {
PathResourceResolver resolver = (PathResourceResolver) this.handler.getResourceResolvers().get(0);
Resource[] locations = resolver.getAllowedLocations();
assertEquals(3, locations.length);
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
assertEquals("testalternatepath/", ((ClassPathResource) locations[1]).getPath());
assertEquals("META-INF/resources/webjars/", ((ClassPathResource) locations[2]).getPath());
}
@Test
public void initAllowedLocationsWithExplicitConfiguration() throws Exception {
ClassPathResource location1 = new ClassPathResource("test/", getClass());
ClassPathResource location2 = new ClassPathResource("testalternatepath/", getClass());
PathResourceResolver pathResolver = new PathResourceResolver();
pathResolver.setAllowedLocations(location1);
ResourceWebHandler handler = new ResourceWebHandler();
handler.setResourceResolvers(Collections.singletonList(pathResolver));
handler.setLocations(Arrays.asList(location1, location2));
handler.afterPropertiesSet();
handler.afterSingletonsInstantiated();
Resource[] locations = pathResolver.getAllowedLocations();
assertEquals(1, locations.length);
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
}
@Test
public void notModified() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("")
.ifModifiedSince(resourceLastModified("test/foo.css")).toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.NOT_MODIFIED, exchange.getResponse().getStatusCode());
}
@Test
public void modified() throws Exception {
long timestamp = resourceLastModified("test/foo.css") / 1000 * 1000 - 1;
MockServerWebExchange exchange = MockServerHttpRequest.get("").ifModifiedSince(timestamp).toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertNull(exchange.getResponse().getStatusCode());
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test
public void directory() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "js/");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
}
@Test
public void directoryInJarFile() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "underscorejs/");
this.handler.handle(exchange).block(TIMEOUT);
assertNull(exchange.getResponse().getStatusCode());
assertEquals(0, exchange.getResponse().getHeaders().getContentLength());
}
@Test
public void missingResourcePath() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
setPathWithinHandlerMapping(exchange, "");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
}
@Test(expected = IllegalStateException.class)
public void noPathWithinHandlerMappingAttribute() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
this.handler.handle(exchange).block(TIMEOUT);
}
@Test(expected = MethodNotAllowedException.class)
public void unsupportedHttpMethod() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.post("").toExchange();
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
}
@Test
public void resourceNotFound() throws Exception {
for (HttpMethod method : HttpMethod.values()) {
resourceNotFound(method);
}
}
private void resourceNotFound(HttpMethod httpMethod) throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.method(httpMethod, "").toExchange();
setPathWithinHandlerMapping(exchange, "not-there.css");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
}
@Test
public void partialContentByteRange() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("Range", "bytes=0-1").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, exchange.getResponse().getHeaders().getContentType());
assertEquals(2, exchange.getResponse().getHeaders().getContentLength());
assertEquals("bytes 0-1/10", exchange.getResponse().getHeaders().getFirst("Content-Range"));
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
assertResponseBody(exchange, "So");
}
@Test
public void partialContentByteRangeNoEnd() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("range", "bytes=9-").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, exchange.getResponse().getHeaders().getContentType());
assertEquals(1, exchange.getResponse().getHeaders().getContentLength());
assertEquals("bytes 9-9/10", exchange.getResponse().getHeaders().getFirst("Content-Range"));
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
assertResponseBody(exchange, ".");
}
@Test
public void partialContentByteRangeLargeEnd() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("range", "bytes=9-10000").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, exchange.getResponse().getHeaders().getContentType());
assertEquals(1, exchange.getResponse().getHeaders().getContentLength());
assertEquals("bytes 9-9/10", exchange.getResponse().getHeaders().getFirst("Content-Range"));
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
assertResponseBody(exchange, ".");
}
@Test
public void partialContentSuffixRange() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("range", "bytes=-1").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, exchange.getResponse().getHeaders().getContentType());
assertEquals(1, exchange.getResponse().getHeaders().getContentLength());
assertEquals("bytes 9-9/10", exchange.getResponse().getHeaders().getFirst("Content-Range"));
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
assertResponseBody(exchange, ".");
}
@Test
public void partialContentSuffixRangeLargeSuffix() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("range", "bytes=-11").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertEquals(MediaType.TEXT_PLAIN, exchange.getResponse().getHeaders().getContentType());
assertEquals(10, exchange.getResponse().getHeaders().getContentLength());
assertEquals("bytes 0-9/10", exchange.getResponse().getHeaders().getFirst("Content-Range"));
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, exchange.getResponse().getHeaders().get("Accept-Ranges").size());
assertResponseBody(exchange, "Some text.");
}
@Test
public void partialContentInvalidRangeHeader() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("range", "bytes=foo bar").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
StepVerifier.create(this.handler.handle(exchange))
.expectNextCount(0)
.expectComplete()
.verify();
assertEquals(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, exchange.getResponse().getStatusCode());
assertEquals("bytes", exchange.getResponse().getHeaders().getFirst("Accept-Ranges"));
}
@Test
public void partialContentMultipleByteRanges() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").header("Range", "bytes=0-1, 4-5, 8-9").toExchange();
setPathWithinHandlerMapping(exchange, "foo.txt");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals(HttpStatus.PARTIAL_CONTENT, exchange.getResponse().getStatusCode());
assertTrue(exchange.getResponse().getHeaders().getContentType().toString()
.startsWith("multipart/byteranges;boundary="));
String boundary = "--" + exchange.getResponse().getHeaders().getContentType().toString().substring(30);
Mono<DataBuffer> reduced = Flux.from(exchange.getResponse().getBody())
.reduce(this.bufferFactory.allocateBuffer(), (previous, current) -> {
previous.write(current);
DataBufferUtils.release(current);
return previous;
});
StepVerifier.create(reduced)
.consumeNextWith(buf -> {
String content = DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8);
String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);
assertEquals(boundary, ranges[0]);
assertEquals("Content-Type: text/plain", ranges[1]);
assertEquals("Content-Range: bytes 0-1/10", ranges[2]);
assertEquals("So", ranges[3]);
assertEquals(boundary, ranges[4]);
assertEquals("Content-Type: text/plain", ranges[5]);
assertEquals("Content-Range: bytes 4-5/10", ranges[6]);
assertEquals(" t", ranges[7]);
assertEquals(boundary, ranges[8]);
assertEquals("Content-Type: text/plain", ranges[9]);
assertEquals("Content-Range: bytes 8-9/10", ranges[10]);
assertEquals("t.", ranges[11]);
})
.expectComplete()
.verify();
}
@Test // SPR-14005
public void doOverwriteExistingCacheControlHeaders() throws Exception {
MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange();
exchange.getResponse().getHeaders().setCacheControl(CacheControl.noStore().getHeaderValue());
setPathWithinHandlerMapping(exchange, "foo.css");
this.handler.handle(exchange).block(TIMEOUT);
assertEquals("max-age=3600", exchange.getResponse().getHeaders().getCacheControl());
}
private void setPathWithinHandlerMapping(ServerWebExchange exchange, String path) {
exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, path);
}
private long resourceLastModified(String resourceName) throws IOException {
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
}
private long resourceLastModifiedDate(String resourceName) throws IOException {
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
}
private void assertResponseBody(MockServerWebExchange exchange, String responseBody) {
StepVerifier.create(exchange.getResponse().getBody())
.consumeNextWith(buf -> assertEquals(responseBody,
DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8)))
.expectComplete()
.verify();
}
}