Files
alfresco-community-repo/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java
Constantin Popa debae96be4 Merged WEBAPP-API (5.2.1) to 5.2.N (5.2.1)
134665 cpopa: APPSREPO-105 : Add an API to download multiple file/folders as a zip
      - test fixes to get rid of unpredictable failures
      - fixes after Gavin's OpenAPI spec review


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/BRANCHES/DEV/5.2.N/root@134674 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2017-01-20 12:57:32 +00:00

574 lines
24 KiB
Java

/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco 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 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.tests;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_EXTENSION;
import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_NAME;
import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.servlet.http.HttpServletResponse;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
import org.alfresco.rest.api.model.AssocChild;
import org.alfresco.rest.api.model.Download;
import org.alfresco.rest.api.nodes.NodesEntityResource;
import org.alfresco.rest.api.tests.client.HttpResponse;
import org.alfresco.rest.api.tests.client.PublicApiException;
import org.alfresco.rest.api.tests.client.RequestContext;
import org.alfresco.rest.api.tests.client.data.Document;
import org.alfresco.rest.api.tests.client.data.Folder;
import org.alfresco.rest.api.tests.util.RestApiUtil;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.service.cmr.download.DownloadStatus;
import org.alfresco.service.cmr.site.SiteVisibility;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.simple.JSONObject;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
/**
* Tests the /downloads API
*
* @author cpopa
*
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestDownloads extends AbstractBaseApiTest
{
private static Log logger = LogFactory.getLog(TestDownloads.class);
private static final int NUMBER_OF_TIMES_TO_RETRY_TEST_CANCEL_STATUS = 5;
private static final int STATUS_CHECK_SLEEP_TIME = 5;
private static final int NUMBER_OF_TIMES_TO_CHECK_STATUS = 200;
public static final String NODES_SECONDARY_CHILDREN = "nodes/%s/secondary-children";
public static final String API_DOWNLOADS = "downloads";
private static final String DOC4_NAME = "docTest4.txt";
private static final String SUB_FOLDER1_NAME = "subFolder1";
private static final String DOC3_NAME = "docTest3.txt";
private static final String FOLDER1_NAME = "folder1";
private static final String FOLDER3_NAME = "folder3";
private static final String ZIPPABLE_DOC1_NAME = "docTest1.txt";
private static final String DUMMY_CONTENT = "dummy content";
private org.alfresco.rest.api.Nodes nodesApi;
private String zippableDocId1;
private String zippableDocId2;
private String zippableDocId3_InFolder1;
private String zippableFolderId1;
private String zippableFolderId2_InFolder1;
private String zippableDocId4_InFolder2;
private String zippableFolderId3;
private String zippableDoc_user2;
@Before
public void setupTest() throws IOException, Exception{
nodesApi = applicationContext.getBean("Nodes", org.alfresco.rest.api.Nodes.class);
setRequestContext(user1);
Document zippableDoc1 = createTextFile(tDocLibNodeId, ZIPPABLE_DOC1_NAME, DUMMY_CONTENT);
zippableDocId1 = zippableDoc1.getId();
zippableDocId2 = createTextFile(tDocLibNodeId, "docTest2", DUMMY_CONTENT).getId();
Folder zippableFolder1 = createFolder(tDocLibNodeId, FOLDER1_NAME);
zippableFolderId1 = zippableFolder1.getId();
zippableDocId3_InFolder1 = createTextFile(zippableFolderId1, DOC3_NAME, DUMMY_CONTENT).getId();
Folder zippableFolder2_InFolder1 = createFolder(zippableFolderId1, SUB_FOLDER1_NAME);
zippableFolderId2_InFolder1 = zippableFolder2_InFolder1.getId();
zippableDocId4_InFolder2 = createTextFile(zippableFolderId2_InFolder1, DOC4_NAME, DUMMY_CONTENT).getId();
Folder zippableFolder3 = createFolder(tDocLibNodeId, FOLDER3_NAME);
zippableFolderId3 = zippableFolder3.getId();
setRequestContext(user2);
String user2Site = createSite ("TestSite B - " + RUNID, SiteVisibility.PRIVATE).getId();
String user2DocLib = getSiteContainerNodeId(user2Site, "documentLibrary");
zippableDoc_user2 = createTextFile(user2DocLib, "user2doc", DUMMY_CONTENT).getId();
setRequestContext(user1);
AssocChild secChild = new AssocChild(zippableDoc1.getId(), ASSOC_TYPE_CM_CONTAINS);
post(format(NODES_SECONDARY_CHILDREN, zippableFolder3.getId()), toJsonAsStringNonNull(secChild), HttpServletResponse.SC_CREATED);
}
/**
* Tests the creation of download nodes.
*
* <p>POST:</p>
* {@literal <host>:<port>/alfresco/api/-default-/private/alfresco/versions/1/downloads}
*
*/
@Test
public void test001CreateDownload() throws Exception
{
//test creating a download with a single file
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
assertPendingDownloadProps(download);
assertValidZipNodeid(download);
assertDoneDownload(download, 1, 13);
//test creating a multiple file archive
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2);
assertPendingDownloadProps(download);
assertValidZipNodeid(download);
assertDoneDownload(download, 2, 26);
//test creating a zero file archive
createDownload(HttpServletResponse.SC_BAD_REQUEST);
//test creating an archive with the same file twice
download = createDownload(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1, zippableDocId1);
//test creating an archive with a folder and a file which is contained in the folder
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
assertPendingDownloadProps(download);
assertValidZipNodeid(download);
assertDoneDownload(download, 3, 39);
//test creating an archive with a file and a folder containing that file but only as a secondary parent child association
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3);
assertPendingDownloadProps(download);
assertValidZipNodeid(download);
assertDoneDownload(download, 2, 26);
//test creating an archive with two files, one of which user1 does not have permissions for
download = createDownload(HttpServletResponse.SC_FORBIDDEN, zippableDocId1, zippableDoc_user2);
}
/**
* Tests retrieving info about a download node
*
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/-default-/private/alfresco/versions/1/downloads/<download_id>}
*
*/
@Test
public void test002GetDownloadInfo() throws Exception
{
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableFolderId2_InFolder1, zippableDocId4_InFolder2);
//test retrieving information about an ongoing download
assertInProgressDownload(download, 4, 52);
//test retrieving information about a finished download
assertDoneDownload(download, 4, 52);
//test retrieving the status of a cancelled download
cancelWithRetry(() ->
{
Download downloadToBeCancelled = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
cancel(downloadToBeCancelled.getId());
assertCancelledDownload(downloadToBeCancelled, 3, 39);
});
}
/**
* Tests canceling a download.
*
* <p>DELETE:</p>
* {@literal <host>:<port>/alfresco/api/-default-/private/alfresco/versions/1/downloads/<download_id>}
*
*/
@Test
public void test003CancelDownload() throws Exception
{
//cancel a running download operation.
cancelWithRetry(()->{
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1, zippableDocId1, zippableDocId2);
cancel(download.getId());
assertCancelledDownload(download, 5, 65);
});
//cancel a completed download - should have no effect
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2);
assertDoneDownload(download, 2, 26);
cancel(download.getId());
Thread.sleep(500);
Download downloadStatus = getDownload(download.getId());
assertTrue("A cancel operation on a DONE download has no effect.", downloadStatus.getStatus().equals(DownloadStatus.Status.DONE));
//cancel a node which is not of a download type
cancel(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1);
//user2 canceling user1 download operation - should not be allowed
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
setRequestContext(user2);
cancel(HttpServletResponse.SC_FORBIDDEN, download.getId());
}
/**
* Tests downloading the content of a download node(a zip) using the /nodes API:
*
* <p>GET:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>/content}
*
*/
@Test
public void test004GetDownloadContent() throws Exception{
//test downloading the content of a 1 file zip
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
assertDoneDownload(download, 1, 13);
HttpResponse response = downloadContent(download);
ZipInputStream zipStream = getZipStreamFromResponse(response);
ZipEntry zipEntry = zipStream.getNextEntry();
assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipEntry.getName());
assertTrue("Zip entry size is not correct", zipEntry.getCompressedSize() <= 13);
assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
zipStream.close();
Map<String, String> responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION,
ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION),
responseHeaders.get("Content-Disposition"));
//test downloading the content of a multiple file zip
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1);
assertDoneDownload(download, 3, 39);
response = downloadContent(download);
zipStream = getZipStreamFromResponse(response);
assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/", zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + DOC3_NAME, zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/", zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/" + DOC4_NAME, zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", DOC3_NAME, zipStream.getNextEntry().getName());
assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
zipStream.close();
responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", DEFAULT_ARCHIVE_NAME,
DEFAULT_ARCHIVE_NAME),
responseHeaders.get("Content-Disposition"));
//test download the content of a zip which has a secondary child
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3);
assertDoneDownload(download, 2, 26);
response = downloadContent(download);
zipStream = getZipStreamFromResponse(response);
assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/", zipStream.getNextEntry().getName());
assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/" + ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName());
assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null);
}
/**
* Tests deleting a download node using the /nodes API:
*
* <p>DELETE:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/nodes/<nodeId>}
*
*/
@Test
public void test005DeleteDownloadNode() throws Exception{
//test deleting a download node
Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
assertDoneDownload(download, 1, 13);
deleteNode(download.getId(), true, HttpServletResponse.SC_NO_CONTENT);
getDownload(download.getId(), HttpServletResponse.SC_NOT_FOUND);
//test user2 deleting a download node created by user1
download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1);
assertDoneDownload(download, 1, 13);
setRequestContext(user2);
deleteNode(download.getId(), true, HttpServletResponse.SC_FORBIDDEN);
assertDoneDownload(download, 1, 13);
}
protected ZipInputStream getZipStreamFromResponse(HttpResponse response)
{
return new ZipInputStream(new ByteArrayInputStream(response.getResponseAsBytes()));
}
protected HttpResponse downloadContent(Download download) throws Exception
{
return getSingle(NodesEntityResource.class, download.getId() + "/content", null, HttpServletResponse.SC_OK);
}
/**
* It may happen that a download is already done before getting a chance to call cancel(). Hence retry a couple of times.
* @param cancelAction
* @throws Exception
*/
private void cancelWithRetry(CancelAction cancelAction) throws Exception{
for(int i = 0; i<=NUMBER_OF_TIMES_TO_RETRY_TEST_CANCEL_STATUS; i++)
{
if (i == NUMBER_OF_TIMES_TO_RETRY_TEST_CANCEL_STATUS)
{
logger.error("Did not manage to test the cancel status, the download node gets to the DONE status too fast.");
}
try
{
cancelAction.run();
}catch(DownloadAlreadyDoneException e)
{
continue;
}
}
}
private void assertDoneDownload(Download download, int expectedFilesAdded, int expectedTotal) throws Exception, InterruptedException
{
assertExpectedStatus(DownloadStatus.Status.DONE, download, "Download should be DONE by now.", downloadStatus ->
{
assertTrue("The number of bytes added in the archive does not match the total", downloadStatus.getBytesAdded() == downloadStatus.getTotalBytes());
assertEquals("The number of files added in the archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getFilesAdded());
assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotalBytes());
assertEquals("The total number of files of the final archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getTotalFiles());
}, null, null);
}
protected void assertCancelledDownload(Download download, int expectedTotalFiles, int expectedTotal) throws PublicApiException, Exception, InterruptedException
{
assertExpectedStatus(DownloadStatus.Status.CANCELLED, download, "Download should be CANCELLED by now.", downloadStatus ->
{
assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getBytesAdded() > 0 && downloadStatus.getBytesAdded() <= downloadStatus.getTotalBytes());
assertTrue("The download has been cancelled, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles());
assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotalBytes());
assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles());
}, DownloadStatus.Status.DONE, downloadStatus->
{
throw new DownloadAlreadyDoneException();
});
}
private void assertInProgressDownload(Download download, int expectedTotalFiles, int expectedTotal) throws Exception, InterruptedException
{
assertExpectedStatus(DownloadStatus.Status.IN_PROGRESS, download, "Download creation is taking too long.Download status should be at least IN_PROGRESS by now.", downloadStatus ->
{
//'done' can be equal to the 'total' even though the status is IN_PROGRESS. See ZipDownloadExporter line 239
assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getBytesAdded() > 0 && downloadStatus.getBytesAdded() <= downloadStatus.getTotalBytes());
assertTrue("The download is in progress, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles());
assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotalBytes());
assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles());
}, DownloadStatus.Status.DONE, downloadStatus ->
{
try
{
assertDoneDownload(download, expectedTotalFiles, expectedTotal);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
});
}
private void assertExpectedStatus(DownloadStatus.Status expectedStatus, Download download, String failMessage, Consumer<Download> assertionsToDo,
DownloadStatus.Status alternateExpectedStatus, Consumer<Download> alternateAssertionsToDo) throws Exception{
for(int i = 0; i<=NUMBER_OF_TIMES_TO_CHECK_STATUS; i++){
if (i == NUMBER_OF_TIMES_TO_CHECK_STATUS)
{
fail(failMessage);
}
Download downloadStatus = getDownload(download.getId());
if (alternateExpectedStatus != null && downloadStatus.getStatus().equals(alternateExpectedStatus))
{
alternateAssertionsToDo.accept(downloadStatus);
break;
}
else if (!downloadStatus.getStatus().equals(expectedStatus))
{
Thread.sleep(STATUS_CHECK_SLEEP_TIME);
}else
{
assertionsToDo.accept(downloadStatus);
break;
}
}
}
protected void setRequestContext(String user)
{
setRequestContext(networkOne.getId(), user, null);
}
private void assertValidZipNodeid(Download download)
{
try{
TenantUtil.runAsUserTenant(new TenantRunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
nodesApi.validateNode(download.getId());
return null;
}
}, user1, networkOne.getId());
}catch(ApiException ex){
org.junit.Assert.fail("The download nodeid is not valid." + ex.getMessage());
}
}
private void assertPendingDownloadProps(Download download)
{
assertEquals("The download request hasn't been processed yet, the status is not correct", DownloadStatus.Status.PENDING, download.getStatus());
assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getBytesAdded());
assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getFilesAdded());
assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotalBytes());
assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotalFiles());
}
@Override
public String getScope()
{
return "public";
}
private Download createDownload(int expectedStatus, String ... nodeIds) throws Exception
{
Download downloadRequest = new Download();
downloadRequest.setNodeIds(Arrays.asList(nodeIds));
setRequestContext(user1);
Download download = create(downloadRequest, expectedStatus);
return download;
}
public Download create(Download download, int expectedStatus) throws Exception
{
HttpResponse response = post(API_DOWNLOADS, RestApiUtil.toJsonAsStringNonNull(download), expectedStatus);
return getDownloadFromResponse(response);
}
public Download getDownload(String downloadId, int expectedStatus) throws Exception
{
HttpResponse response = getSingle(API_DOWNLOADS, downloadId, expectedStatus);
return getDownloadFromResponse(response);
}
public Download getDownload(String downloadId) throws Exception
{
return getDownload(downloadId, HttpServletResponse.SC_OK);
}
public void cancel(String downloadId) throws Exception
{
cancel(HttpServletResponse.SC_ACCEPTED, downloadId);
}
public void cancel(int expectedStatusCode, String downloadId) throws Exception
{
delete(API_DOWNLOADS, downloadId, expectedStatusCode);
}
protected Download getDownloadFromResponse(HttpResponse response) throws Exception
{
if (asList(SC_ACCEPTED, SC_OK).contains(response.getStatusCode()))
{
return RestApiUtil.parseRestApiEntry((JSONObject) response.getJsonResponse(), Download.class);
}
return null;
}
private static class DownloadAlreadyDoneException extends RuntimeException
{
}
private interface CancelAction{
void run() throws Exception;
}
}