diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java index 3aa378980..31fe9a64e 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java @@ -11,12 +11,12 @@ package org.cryptomator.filesystem.inmem; import java.io.FileNotFoundException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.nio.file.FileAlreadyExistsException; import java.time.Instant; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; -import org.apache.commons.io.FileExistsException; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; @@ -53,12 +53,15 @@ class InMemoryFile extends InMemoryNode implements File { writeLock.lock(); final InMemoryFolder parent = parent().get(); parent.existingChildren.compute(this.name(), (k, v) -> { - if (v == null || v == this) { - this.lastModified = Instant.now(); - this.creationTime = Instant.now(); - return this; + if (v != null && v != this) { + // other file or folder with same name already exists. + throw new UncheckedIOException(new FileAlreadyExistsException(k)); } else { - throw new UncheckedIOException(new FileExistsException(k)); + if (v == null) { + this.creationTime = Instant.now(); + } + this.lastModified = Instant.now(); + return this; } }); return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock); diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java index 50988d10a..d6bfb5d1d 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java @@ -15,7 +15,7 @@ import java.io.UncheckedIOException; import java.time.Instant; import java.util.Iterator; import java.util.Map; -import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.apache.commons.io.FileExistsException; @@ -24,7 +24,7 @@ import org.cryptomator.filesystem.Folder; class InMemoryFolder extends InMemoryNode implements Folder { - final Map existingChildren = new TreeMap<>(); + final Map existingChildren = new ConcurrentHashMap<>(); private final WeakValuedCache folders = WeakValuedCache.usingLoader(this::newFolder); private final WeakValuedCache files = WeakValuedCache.usingLoader(this::newFile); @@ -67,11 +67,12 @@ class InMemoryFolder extends InMemoryNode implements Folder { } parent.create(); parent.existingChildren.compute(name, (k, v) -> { - if (v == null) { + if (v != null) { + // other file or folder with same name already exists. + throw new UncheckedIOException(new FileExistsException(k)); + } else { this.lastModified = Instant.now(); return this; - } else { - throw new UncheckedIOException(new FileExistsException(k)); } }); assert this.exists(); @@ -83,11 +84,11 @@ class InMemoryFolder extends InMemoryNode implements Folder { if (target.exists()) { target.delete(); } - assert !target.exists(); + assert!target.exists(); target.create(); this.copyTo(target); this.delete(); - assert !this.exists(); + assert!this.exists(); } @Override @@ -109,7 +110,12 @@ class InMemoryFolder extends InMemoryNode implements Folder { subFolder.delete(); } } - assert !this.exists(); + assert!this.exists(); + } + + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + creationTime = instant; } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java index 868d6d0be..2ad16d7bb 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavServletContextFactory.java @@ -20,6 +20,7 @@ import org.apache.commons.lang3.SystemUtils; import org.cryptomator.filesystem.Folder; import org.cryptomator.webdav.filters.AcceptRangeFilter; import org.cryptomator.webdav.filters.MacChunkedPutCompatibilityFilter; +import org.cryptomator.webdav.filters.MkcolComplianceFilter; import org.cryptomator.webdav.filters.UriNormalizationFilter; import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker; import org.cryptomator.webdav.filters.UriNormalizationFilter.ResourceTypeChecker.ResourceType; @@ -64,6 +65,7 @@ class WebDavServletContextFactory { final ServletContextHandler servletContext = new ServletContextHandler(null, contextPath, ServletContextHandler.SESSIONS); final ServletHolder servletHolder = new ServletHolder(contextPath, new WebDavServlet(contextRoot, root)); servletContext.addServlet(servletHolder, WILDCARD); + servletContext.addFilter(MkcolComplianceFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(AcceptRangeFilter.class, WILDCARD, EnumSet.of(DispatcherType.REQUEST)); servletContext.addFilter(new FilterHolder(new UriNormalizationFilter(resourceTypeChecker)), WILDCARD, EnumSet.of(DispatcherType.REQUEST)); if (SystemUtils.IS_OS_MAC_OSX) { diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/MkcolComplianceFilter.java b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/MkcolComplianceFilter.java new file mode 100644 index 000000000..59dd6f830 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/MkcolComplianceFilter.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.webdav.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Responds with status code 415, if an attempt is made to create a collection with a body. + * + * See https://tools.ietf.org/html/rfc2518#section-8.3.1: + * "If the server receives a MKCOL request entity type it does not support or understand + * it MUST respond with a 415 (Unsupported Media Type) status code." + */ +public class MkcolComplianceFilter implements HttpFilter { + + private static final String METHOD_MKCOL = "MKCOL"; + private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no-op + } + + @Override + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + boolean hasBody = request.getContentLengthLong() > 0 || request.getHeader(HEADER_TRANSFER_ENCODING) != null; + if (METHOD_MKCOL.equalsIgnoreCase(request.getMethod()) && hasBody) { + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "MKCOL with body not supported."); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // no-op + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java index e3a2fcd8e..1716cae60 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java @@ -9,7 +9,7 @@ package org.cryptomator.webdav.filters; import java.io.IOException; -import java.util.function.Function; +import java.net.URI; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -37,68 +37,11 @@ public class UriNormalizationFilter implements HttpFilter { private static final String[] FILE_METHODS = {"PUT"}; private static final String[] DIRECTORY_METHODS = {"MKCOL"}; - private final ResourceTypeChecker resourceTypeChecker; - - public UriNormalizationFilter(ResourceTypeChecker resourceTypeChecker) { - this.resourceTypeChecker = resourceTypeChecker; - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // no-op - } - - @Override - public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - ResourceType resourceType = resourceTypeChecker.typeOfResource(request.getPathInfo()); - HttpServletRequest normalizedRequest = resourceType.normalizedRequest(request); - chain.doFilter(normalizedRequest, response); - } - - @Override - public void destroy() { - // no-op - } - - private static HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) { - LOG.debug("Treating resource as file: {}", originalRequest.getRequestURI()); - return new FileUriRequest(originalRequest); - } - - private static HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) { - LOG.debug("Treating resource as folder: {}", originalRequest.getRequestURI()); - return new FolderUriRequest(originalRequest); - } - - private static HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) { - final String requestMethod = originalRequest.getMethod().toUpperCase(); - if (ArrayUtils.contains(FILE_METHODS, requestMethod)) { - return normalizedFileRequest(originalRequest); - } else if (ArrayUtils.contains(DIRECTORY_METHODS, requestMethod)) { - return normalizedFolderRequest(originalRequest); - } else { - LOG.debug("Could not determine resource type of resource: {}", originalRequest.getRequestURI()); - return originalRequest; - } - } - @FunctionalInterface public interface ResourceTypeChecker { enum ResourceType { - FILE(UriNormalizationFilter::normalizedFileRequest), // - FOLDER(UriNormalizationFilter::normalizedFolderRequest), // - UNKNOWN(UriNormalizationFilter::normalizedRequestForUnknownResource); - - private final Function wrapper; - - private ResourceType(Function wrapper) { - this.wrapper = wrapper; - } - - private HttpServletRequest normalizedRequest(HttpServletRequest request) { - return wrapper.apply(request); - } + FILE, FOLDER, UNKNOWN; }; /** @@ -111,10 +54,67 @@ public class UriNormalizationFilter implements HttpFilter { } + private final ResourceTypeChecker resourceTypeChecker; + private String contextPath; + + public UriNormalizationFilter(ResourceTypeChecker resourceTypeChecker) { + this.resourceTypeChecker = resourceTypeChecker; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + contextPath = filterConfig.getServletContext().getContextPath(); + } + + @Override + public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + final ResourceType resourceType = resourceTypeChecker.typeOfResource(request.getPathInfo()); + final HttpServletRequest normalizedRequest; + switch (resourceType) { + case FILE: + normalizedRequest = normalizedFileRequest(request); + break; + case FOLDER: + normalizedRequest = normalizedFolderRequest(request); + break; + default: + normalizedRequest = normalizedRequestForUnknownResource(request); + break; + } + chain.doFilter(normalizedRequest, response); + } + + @Override + public void destroy() { + // no-op + } + + private HttpServletRequest normalizedFileRequest(HttpServletRequest originalRequest) { + LOG.trace("Treating resource as file: {}", originalRequest.getRequestURI()); + return new FileUriRequest(originalRequest); + } + + private HttpServletRequest normalizedFolderRequest(HttpServletRequest originalRequest) { + LOG.trace("Treating resource as folder: {}", originalRequest.getRequestURI()); + return new FolderUriRequest(originalRequest); + } + + private HttpServletRequest normalizedRequestForUnknownResource(HttpServletRequest originalRequest) { + final String requestMethod = originalRequest.getMethod().toUpperCase(); + if (ArrayUtils.contains(FILE_METHODS, requestMethod)) { + return normalizedFileRequest(originalRequest); + } else if (ArrayUtils.contains(DIRECTORY_METHODS, requestMethod)) { + return normalizedFolderRequest(originalRequest); + } else { + LOG.debug("Could not determine resource type of resource: {}", originalRequest.getRequestURI()); + return originalRequest; + } + } + /** * Adjusts headers containing URIs depending on the request URI. */ - private static class SuffixPreservingRequest extends HttpServletRequestWrapper { + private class SuffixPreservingRequest extends HttpServletRequestWrapper { private static final String HEADER_DESTINATION = "Destination"; private static final String METHOD_MOVE = "MOVE"; @@ -122,32 +122,53 @@ public class UriNormalizationFilter implements HttpFilter { public SuffixPreservingRequest(HttpServletRequest request) { super(request); + request.getContextPath(); } @Override public String getHeader(String name) { if ((METHOD_MOVE.equalsIgnoreCase(getMethod()) || METHOD_COPY.equalsIgnoreCase(getMethod())) && HEADER_DESTINATION.equalsIgnoreCase(name)) { - return sameSuffixAsUri(super.getHeader(name)); + final String uri = URI.create(super.getHeader(name)).getPath(); + return bestGuess(uri); } else { return super.getHeader(name); } } - private String sameSuffixAsUri(String str) { - final String uri = this.getRequestURI(); - if (uri.endsWith("/")) { - return StringUtils.appendIfMissing(str, "/"); - } else { - return StringUtils.removeEnd(str, "/"); + private String bestGuess(String uri) { + final String pathWithinContext = StringUtils.removeStart(uri, contextPath); + final ResourceType resourceType = resourceTypeChecker.typeOfResource(pathWithinContext); + switch (resourceType) { + case FILE: + System.out.println("DST is file " + uri); + return asFileUri(uri); + case FOLDER: + System.out.println("DST is folder " + uri); + return asFolderUri(uri); + default: + System.out.println("DST doesn't exist " + uri); + if (this.getRequestURI().endsWith("/")) { + return asFolderUri(uri); + } else { + return asFileUri(uri); + } } } + protected String asFileUri(String uri) { + return StringUtils.removeEnd(uri, "/"); + } + + protected String asFolderUri(String uri) { + return StringUtils.appendIfMissing(uri, "/"); + } + } /** * HTTP request, whose URI never ends on "/". */ - private static class FileUriRequest extends SuffixPreservingRequest { + private class FileUriRequest extends SuffixPreservingRequest { public FileUriRequest(HttpServletRequest request) { super(request); @@ -155,7 +176,7 @@ public class UriNormalizationFilter implements HttpFilter { @Override public String getRequestURI() { - return StringUtils.removeEnd(super.getRequestURI(), "/"); + return asFileUri(super.getRequestURI()); } } @@ -163,7 +184,7 @@ public class UriNormalizationFilter implements HttpFilter { /** * HTTP request, whose URI always ends on "/". */ - private static class FolderUriRequest extends SuffixPreservingRequest { + private class FolderUriRequest extends SuffixPreservingRequest { public FolderUriRequest(HttpServletRequest request) { super(request); @@ -171,7 +192,7 @@ public class UriNormalizationFilter implements HttpFilter { @Override public String getRequestURI() { - return StringUtils.appendIfMissing(super.getRequestURI(), "/"); + return asFolderUri(super.getRequestURI()); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java index 68092de29..785ad48ec 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java @@ -17,6 +17,7 @@ import java.util.Optional; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; @@ -25,6 +26,8 @@ import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertySet; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; import org.cryptomator.filesystem.jackrabbit.FileLocator; @@ -73,7 +76,23 @@ class DavFile extends DavNode { public void move(DavResource destination) throws DavException { if (destination instanceof DavFile) { DavFile dst = (DavFile) destination; + if (dst.node.exists()) { + // Overwrite header already checked by AbstractWebdavServlet#validateDestination + dst.node.delete(); + } else if (!dst.node.parent().get().exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } node.moveTo(dst.node); + } else if (destination instanceof DavFolder) { + DavFolder dst = (DavFolder) destination; + Folder parent = dst.node.parent().get(); + File newDst = parent.file(dst.node.name()); + if (dst.node.exists()) { + dst.node.delete(); + } else if (!parent.exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } + node.moveTo(newDst); } else { throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); } @@ -83,9 +102,25 @@ class DavFile extends DavNode { public void copy(DavResource destination, boolean shallow) throws DavException { if (destination instanceof DavFile) { DavFile dst = (DavFile) destination; + if (dst.node.exists()) { + // Overwrite header already checked by AbstractWebdavServlet#validateDestination + dst.node.delete(); + } else if (!dst.node.parent().get().exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } node.copyTo(dst.node); + } else if (destination instanceof DavFolder) { + DavFolder dst = (DavFolder) destination; + Folder parent = dst.node.parent().get(); + File newDst = parent.file(dst.node.name()); + if (dst.node.exists()) { + dst.node.delete(); + } else if (!parent.exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } + node.copyTo(newDst); } else { - throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); + throw new IllegalArgumentException("Destination not a DavFile: " + destination.getClass().getName()); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java index f23880032..0534606bb 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java @@ -124,7 +124,22 @@ class DavFolder extends DavNode { public void move(DavResource destination) throws DavException { if (destination instanceof DavFolder) { DavFolder dst = (DavFolder) destination; + if (dst.node.exists()) { + dst.node.delete(); + } else if (!dst.node.parent().get().exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } node.moveTo(dst.node); + } else if (destination instanceof DavFile) { + DavFile dst = (DavFile) destination; + Folder parent = dst.node.parent().get(); + Folder newDst = parent.folder(dst.node.name()); + if (dst.node.exists()) { + dst.node.delete(); + } else if (!parent.exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } + node.moveTo(newDst); } else { throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); } @@ -132,11 +147,31 @@ class DavFolder extends DavNode { @Override public void copy(DavResource destination, boolean shallow) throws DavException { - if (shallow) { - throw new UnsupportedOperationException("Shallow copy of directories not supported."); - } else if (destination instanceof DavFolder) { + if (destination instanceof DavFolder) { DavFolder dst = (DavFolder) destination; - node.copyTo(dst.node); + if (dst.node.exists()) { + dst.node.delete(); + } else if (!dst.node.parent().get().exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } + dst.node.create(); + if (shallow) { + // http://www.webdav.org/specs/rfc2518.html#copy.for.collections + node.creationTime().ifPresent(dst::setCreationTime); + dst.setModificationTime(node.lastModified()); + } else { + node.copyTo(dst.node); + } + } else if (destination instanceof DavFile) { + DavFile dst = (DavFile) destination; + Folder parent = dst.node.parent().get(); + Folder newDst = parent.folder(dst.node.name()); + if (dst.node.exists()) { + dst.node.delete(); + } else if (!parent.exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); + } + node.copyTo(newDst); } else { throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); } diff --git a/main/frontend-webdav/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java b/main/frontend-webdav/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java index e6900d1d4..d53bf9ab6 100644 --- a/main/frontend-webdav/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java +++ b/main/frontend-webdav/src/test/java/org/cryptomator/webdav/filters/UriNormalizationFilterTest.java @@ -126,6 +126,36 @@ public class UriNormalizationFilterTest { Assert.assertEquals("/404/", wrappedReq.getValue().getHeader("Destination")); } + /* MIXED */ + + @Test + public void testCopyFileToFolderRequest() throws IOException, ServletException { + Mockito.when(request.getPathInfo()).thenReturn("/file/"); + Mockito.when(request.getRequestURI()).thenReturn("/file/"); + Mockito.when(request.getMethod()).thenReturn("COPY"); + Mockito.when(request.getHeader("Destination")).thenReturn("/folder"); + filter.doFilter(request, response, chain); + + ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); + Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); + Assert.assertEquals("/file", wrappedReq.getValue().getRequestURI()); + Assert.assertEquals("/folder/", wrappedReq.getValue().getHeader("Destination")); + } + + @Test + public void testMoveFolderToFileRequest() throws IOException, ServletException { + Mockito.when(request.getPathInfo()).thenReturn("/folder"); + Mockito.when(request.getRequestURI()).thenReturn("/folder"); + Mockito.when(request.getMethod()).thenReturn("COPY"); + Mockito.when(request.getHeader("Destination")).thenReturn("/file/"); + filter.doFilter(request, response, chain); + + ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); + Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); + Assert.assertEquals("/folder/", wrappedReq.getValue().getRequestURI()); + Assert.assertEquals("/file", wrappedReq.getValue().getHeader("Destination")); + } + /* UNKNOWN */ @Test