/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.web; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Date; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.xwiki.model.reference.AttachmentReference; import org.xwiki.model.reference.DocumentReference; import org.xwiki.resource.ResourceReference; import org.xwiki.resource.ResourceReferenceManager; import org.xwiki.resource.entity.EntityResourceAction; import org.xwiki.resource.entity.EntityResourceReference; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.plugin.XWikiPluginManager; import com.xpn.xwiki.test.MockitoOldcoreRule; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Validate {@link DownloadAction}. * * @version $Id: b25c2cf5870495425da1b3f35603ed4d61bad4eb $ */ public class DownloadActionTest { /** The name of the attachment being downloaded in most of the tests. */ private static final String DEFAULT_FILE_NAME = "file.txt"; /** The URI requested in most of the tests. */ private static final String DEFAULT_URI = "/xwiki/bin/download/space/page/file.txt"; @Rule public MockitoOldcoreRule oldcore = new MockitoOldcoreRule(); /** Mocked context document. */ private XWikiDocument document; /** Mocked client request. */ private XWikiRequest request; /** Mocked client response. */ private XWikiResponse response; /** Mocked engine context. */ private XWikiEngineContext ec; /** A mocked output stream where the output file data is being written. */ private ServletOutputStream out; /** The action being tested. */ private DownloadAction action = new DownloadAction(); /** The content of the file being downloaded in most of the tests. */ private byte[] fileContent = "abcdefghijklmn".getBytes(XWiki.DEFAULT_ENCODING); private ResourceReferenceManager resourceReferenceManager; private DocumentReference documentReference; /** * Default constructor. * * @throws UnsupportedEncodingException if UTF-8 is not available, so never */ public DownloadActionTest() throws UnsupportedEncodingException { // Empty, needed for declaring the exception thrown while initializing fileContent } @Before public void before() throws Exception { this.oldcore.registerMockEnvironment(); this.request = mock(XWikiRequest.class); this.oldcore.getXWikiContext().setRequest(this.request); this.response = mock(XWikiResponse.class); this.oldcore.getXWikiContext().setResponse(this.response); this.ec = mock(XWikiEngineContext.class); this.oldcore.getXWikiContext().setEngineContext(this.ec); this.out = mock(ServletOutputStream.class); XWikiPluginManager pluginManager = new XWikiPluginManager(); pluginManager.initInterface(); this.documentReference = new DocumentReference("wiki", "space", "page"); this.document = new XWikiDocument(this.documentReference); this.oldcore.getXWikiContext().setDoc(this.document); doReturn(pluginManager).when(this.oldcore.getSpyXWiki()).getPluginManager(); when(this.ec.getMimeType("file.txt")).thenReturn("text/plain"); when(this.response.getOutputStream()).thenReturn(this.out); when(this.oldcore.getMockRightService().hasAccessLevel(eq("programming"), any(), any(), any(XWikiContext.class))).thenReturn(false); // Mock what's needed for extracting the filename from the URL this.resourceReferenceManager = this.oldcore.getMocker().registerMockComponent(ResourceReferenceManager.class); } @Test public void downloadNormal() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, null, -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenIfModifiedSinceBefore() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, null, d.getTime() - 1000l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenIfModifiedSinceSame() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, null, d.getTime(), DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); } @Test public void downloadWhenIfModifiedSinceAfter() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, null, d.getTime() + 1000l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); } @Test(expected = XWikiException.class) public void downloadWhenMissingFile() throws XWikiException { setRequestExpectations("/xwiki/bin/download/space/page/nofile.txt", null, null, null, -1l, DEFAULT_FILE_NAME); this.action.render(this.oldcore.getXWikiContext()); } @Test public void downloadWhenURLNotPointingToAttachment() throws XWikiException { ResourceReference rr = new EntityResourceReference(this.documentReference, EntityResourceAction.VIEW); when(this.resourceReferenceManager.getResourceReference()).thenReturn(rr); when(this.request.getRequestURI()).thenReturn("/xwiki/bin/download/space/page"); try { this.action.render(this.oldcore.getXWikiContext()); fail("Should have thrown an exception before reaching here"); } catch (XWikiException expected) { assertEquals("Error number 11003 in 11: Attachment not found", expected.getMessage()); } } @Test public void downloadById() throws XWikiException, IOException { Date d = new Date(); XWikiAttachment att; for (byte i = 0; i < 10; ++i) { att = new XWikiAttachment(this.document, "file." + i + ".txt"); byte[] content = new byte[1]; content[0] = (byte) ('0' + i); att.setContent(new ByteArrayInputStream(content)); att.setDate(d); this.document.getAttachmentList().add(att); } when(this.ec.getMimeType("file.5.txt")).thenReturn("text/plain"); setRequestExpectations("/xwiki/bin/download/space/page/file.2.txt", "5", null, null, -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), 1, "text/plain", "inline; filename*=utf-8''file.5.txt"); verify(this.out).write(argThat(new ArgumentMatcher<byte[]>() { @Override public boolean matches(byte[] argument) { return argument[0] == '5'; } }), eq(0), eq(1)); verify(this.out).write(argThat(new ArgumentMatcher<byte[]>() { @Override public boolean matches(byte[] argument) { return argument[0] == '5'; } }), eq(0), eq(1)); } @Test(expected = XWikiException.class) public void testDownloadByWrongId() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, "42", null, null, -1l, DEFAULT_FILE_NAME); this.action.render(this.oldcore.getXWikiContext()); } @Test public void downloadWhenInvalidId() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, "two", null, null, -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test(expected = XWikiException.class) public void downloadWhenIncompletePath() throws XWikiException { setRequestExpectations("/xwiki/bin/download/", null, null, null, -1l, DEFAULT_FILE_NAME); this.action.render(this.oldcore.getXWikiContext()); } @Test public void downloadWhenDifferentMimeType() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, "file.png"); when(this.ec.getMimeType("file.png")).thenReturn("image/png"); setRequestExpectations("/xwiki/bin/download/space/page/file.png", null, null, null, -1l, "file.png"); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length, "image/png", "inline; filename*=utf-8''file.png"); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenForce() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, "1", null, -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length, "text/plain", "attachment; filename*=utf-8''file.txt"); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWithRealDate() throws XWikiException, IOException { Date d = new Date(411757300000l); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, null, -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenNameWithSpacesEncodedWithPlus() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, "file name.txt"); when(this.ec.getMimeType("file name.txt")).thenReturn("text/plain"); setRequestExpectations("/xwiki/bin/download/space/page/file+name.txt", null, null, null, -1l, "file name.txt"); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length, "text/plain", "inline; filename*=utf-8''file%20name.txt"); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenNameWithSpacesEncodedWithPercent() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, "file name.txt"); when(this.ec.getMimeType("file name.txt")).thenReturn("text/plain"); setRequestExpectations("/xwiki/bin/download/space/page/file%20name.txt", null, "1", null, -1l, "file name.txt"); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length, "text/plain", "attachment; filename*=utf-8''file%20name.txt"); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenNameWithNonAsciiChars() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, "file\u021B.txt"); when(this.ec.getMimeType("file\u021B.txt")).thenReturn("text/plain"); setRequestExpectations("/xwiki/bin/download/space/page/file%C8%9B.txt", null, "1", null, -1l, "file\u021B.txt"); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length, "text/plain", "attachment; filename*=utf-8''file%C8%9B.txt"); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenValidStartRange() throws XWikiException, IOException { // This test expects bytes 0, 1, 2 and 3 from the file. Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=0-3", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 0-3/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), 4); verifyOutputExpectations(0, 4); } @Test public void downloadWhenValidMiddleRange() throws XWikiException, IOException { // This test expects bytes 3, 4 and 5 from the file. Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=3-5", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 3-5/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), 3); verifyOutputExpectations(3, 6); } @Test public void downloadWhenValidEndRange() throws XWikiException, IOException { // This test expects bytes 9, 10, 11, 12 and 13 from the file. Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=9-13", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 9-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length - 9); verifyOutputExpectations(9, this.fileContent.length); } @Test public void downloadWhenOneByteRange() throws XWikiException, IOException { // This test expects the last four bytes (10, 11, 12 and 13) from the file Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=0-0", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 0-0/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), 1); verifyOutputExpectations(0, 1); } @Test public void downloadWhenRestRange() throws XWikiException, IOException { // This test expects bytes from 11 to the end of the file (11, 12 and 13) Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=11-", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 11-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length - 11); verifyOutputExpectations(11, this.fileContent.length); } @Test public void downloadWhenFullRestRange() throws XWikiException, IOException { // This test expects the whole file Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=0-", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 0-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenTailRange() throws XWikiException, IOException { // This test expects the last four bytes (10, 11, 12 and 13) from the file Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=-4", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 10-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length - 10); verifyOutputExpectations(10, this.fileContent.length); } @Test public void downloadWhenFullTailRange() throws XWikiException, IOException { // This test expects the whole file Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=-14", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 0-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenOverflowingTailRange() throws XWikiException, IOException { // This test expects the whole file, although the client requested more Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=-40", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 0-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenValidOverflowingRange() throws XWikiException, IOException { // This test expects bytes 9, 10, 11, 12 and 13 from the file, although 14 and 15 are requested as well. Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=9-15", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); verify(this.response).setHeader("Content-Range", "bytes 9-13/" + DownloadActionTest.this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length - 9); verifyOutputExpectations(9, this.fileContent.length); } @Test public void downloadWhenInvalidSwappedRange() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=9-5", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenInvalidRange() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=all", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyResponseExpectations(d.getTime(), this.fileContent.length); verifyOutputExpectations(0, this.fileContent.length); } @Test public void downloadWhenOutsideRange() throws XWikiException, IOException { // This test expects a 416 response Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=129-145", -1l, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } @Test public void downloadWhenOutsideRestRange() throws XWikiException, IOException { // This test expects a 416 response Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=129-", -1L, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verify(this.response).setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } @Test public void downloadWhenEmptyRange() throws XWikiException, IOException { Date d = new Date(); createAttachment(d, DEFAULT_FILE_NAME); setRequestExpectations(DEFAULT_URI, null, null, "bytes=-", -1L, DEFAULT_FILE_NAME); assertNull(this.action.render(this.oldcore.getXWikiContext())); verifyOutputExpectations(0, this.fileContent.length); verifyResponseExpectations(d.getTime(), this.fileContent.length); } private void createAttachment(Date d, String name) throws IOException { XWikiAttachment filetxt = new XWikiAttachment(this.document, name); filetxt.setContent(new ByteArrayInputStream(this.fileContent)); filetxt.setDate(d); this.document.getAttachmentList().add(filetxt); } private void setRequestExpectations(String uri, String id, String forceDownload, String range, long modifiedSince, String attachmentName) { ResourceReference rr = new EntityResourceReference( new AttachmentReference(attachmentName, this.documentReference), EntityResourceAction.VIEW); when(this.request.getRequestURI()).thenReturn(uri); when(this.request.getParameter("id")).thenReturn(id); when(this.request.getDateHeader("If-Modified-Since")).thenReturn(modifiedSince); when(this.request.getParameter("force-download")).thenReturn(forceDownload); when(this.request.getHeader("Range")).thenReturn(range); when(this.resourceReferenceManager.getResourceReference()).thenReturn(rr); } private void verifyResponseExpectations(long modified, int length) { verifyResponseExpectations(modified, length, "text/plain", "inline; filename*=utf-8''file.txt"); } private void verifyResponseExpectations(long modified, int length, String mime, String disposition) { verify(this.response).setContentType(mime); verify(this.response).setHeader("Accept-Ranges", "bytes"); verify(this.response).addHeader("Content-disposition", disposition); verify(this.response).setDateHeader("Last-Modified", modified); verify(this.response).setContentLength(length); } private void verifyOutputExpectations(final int start, final int end) throws IOException { verify(this.out).write(argThat(new ArgumentMatcher<byte[]>() { @Override public boolean matches(byte[] argument) { for (int i = start; i < end; ++i) { if (argument[i - start] != DownloadActionTest.this.fileContent[i]) { return false; } } return true; } }), eq(0), eq(end - start)); } }