package edu.harvard.iq.dataverse.api; import com.jayway.restassured.http.ContentType; import com.jayway.restassured.path.json.JsonPath; import com.jayway.restassured.response.Response; import java.util.UUID; import java.util.logging.Logger; import javax.json.Json; import javax.json.JsonObjectBuilder; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.logging.Level; import edu.harvard.iq.dataverse.api.datadeposit.SwordConfigurationImpl; import com.jayway.restassured.path.xml.XmlPath; import org.junit.Test; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.path.xml.XmlPath.from; import java.math.BigDecimal; import java.util.List; import javax.json.JsonValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; public class UtilIT { private static final Logger logger = Logger.getLogger(UtilIT.class.getCanonicalName()); public static final String API_TOKEN_HTTP_HEADER = "X-Dataverse-key"; private static final String USERNAME_KEY = "userName"; private static final String EMAIL_KEY = "email"; private static final String API_TOKEN_KEY = "apiToken"; private static final String BUILTIN_USER_KEY = "burrito"; private static final String EMPTY_STRING = ""; private static SwordConfigurationImpl swordConfiguration = new SwordConfigurationImpl(); static String getRestAssuredBaseUri() { String saneDefaultInDev = "http://localhost:8080"; String restAssuredBaseUri = saneDefaultInDev; String specifiedUri = System.getProperty("dataverse.test.baseurl"); if (specifiedUri != null) { restAssuredBaseUri = specifiedUri; } logger.info("Base URL for tests: " + restAssuredBaseUri); return restAssuredBaseUri; } public static Response createRandomUser() { String randomString = getRandomUsername(); logger.info("Creating random test user " + randomString); String userAsJson = getUserAsJsonString(randomString, randomString, randomString); String password = getPassword(userAsJson); Response response = given() .body(userAsJson) .contentType(ContentType.JSON) .post("/api/builtin-users?key=" + BUILTIN_USER_KEY + "&password=" + password); return response; } private static String getUserAsJsonString(String username, String firstName, String lastName) { JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add(USERNAME_KEY, username); builder.add("firstName", firstName); builder.add("lastName", lastName); builder.add(EMAIL_KEY, getEmailFromUserName(username)); String userAsJson = builder.build().toString(); logger.fine("User to create: " + userAsJson); return userAsJson; } private static String getEmailFromUserName(String username) { return username + "@mailinator.com"; } private static String getPassword(String jsonStr) { String password = JsonPath.from(jsonStr).get(USERNAME_KEY); return password; } private static String getRandomUsername() { return "user" + getRandomIdentifier().substring(0, 8); } public static String getRandomIdentifier() { return UUID.randomUUID().toString().substring(0, 8); } static String getUsernameFromResponse(Response createUserResponse) { JsonPath createdUser = JsonPath.from(createUserResponse.body().asString()); String username = createdUser.getString("data.user." + USERNAME_KEY); logger.info("Username found in create user response: " + username); return username; } static String getApiTokenFromResponse(Response createUserResponse) { JsonPath createdUser = JsonPath.from(createUserResponse.body().asString()); String apiToken = createdUser.getString("data." + API_TOKEN_KEY); logger.info("API token found in create user response: " + apiToken); return apiToken; } static String getAliasFromResponse(Response createDataverseResponse) { JsonPath createdDataverse = JsonPath.from(createDataverseResponse.body().asString()); String alias = createdDataverse.getString("data.alias"); logger.info("Alias found in create dataverse response: " + alias); return alias; } static Integer getDatasetIdFromResponse(Response createDatasetResponse) { JsonPath createdDataset = JsonPath.from(createDatasetResponse.body().asString()); int datasetId = createdDataset.getInt("data.id"); logger.info("Id found in create dataset response: " + datasetId); return datasetId; } static String getDatasetPersistentIdFromResponse(Response createDatasetResponse) { String xml = createDatasetResponse.body().asString(); String datasetSwordIdUrl = from(xml).get("entry.id"); /** * @todo stop assuming the last 22 characters are the doi/globalId */ return datasetSwordIdUrl.substring(datasetSwordIdUrl.length() - 22); } public static Response getServiceDocument(String apiToken) { Response response = given() .auth().basic(apiToken, EMPTY_STRING) .get(swordConfiguration.getBaseUrlPathCurrent() + "/service-document"); return response; } static Response createDataverse(String alias, String category, String apiToken) { JsonArrayBuilder contactArrayBuilder = Json.createArrayBuilder(); contactArrayBuilder.add(Json.createObjectBuilder().add("contactEmail", getEmailFromUserName(getRandomIdentifier()))); JsonArrayBuilder subjectArrayBuilder = Json.createArrayBuilder(); subjectArrayBuilder.add("Other"); JsonObject dvData = Json.createObjectBuilder() .add("alias", alias) .add("name", alias) .add("dataverseContacts", contactArrayBuilder) .add("dataverseSubjects", subjectArrayBuilder) // don't send "dataverseType" if category is null, must be a better way .add(category != null ? "dataverseType" : "notTheKeyDataverseType", category != null ? category : "whatever") .build(); Response createDataverseResponse = given() .body(dvData.toString()).contentType(ContentType.JSON) .when().post("/api/dataverses/:root?key=" + apiToken); return createDataverseResponse; } static Response createDataverse(JsonObject dvData, String apiToken) { Response createDataverseResponse = given() .body(dvData.toString()).contentType(ContentType.JSON) .when().post("/api/dataverses/:root?key=" + apiToken); return createDataverseResponse; } static Response createRandomDataverse(String apiToken) { String alias = getRandomIdentifier(); String category = null; return createDataverse(alias, category, apiToken); } static Response showDataverseContents(String alias, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .when().get("/api/dataverses/" + alias + "/contents"); } static Response createRandomDatasetViaNativeApi(String dataverseAlias, String apiToken) { String jsonIn = getDatasetJson(); Response createDatasetResponse = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .body(jsonIn) .contentType("application/json") .post("/api/dataverses/" + dataverseAlias + "/datasets"); return createDatasetResponse; } private static String getDatasetJson() { File datasetVersionJson = new File("scripts/search/tests/data/dataset-finch1.json"); try { String datasetVersionAsJson = new String(Files.readAllBytes(Paths.get(datasetVersionJson.getAbsolutePath()))); return datasetVersionAsJson; } catch (IOException ex) { Logger.getLogger(UtilIT.class.getName()).log(Level.SEVERE, null, ex); return null; } } static Response createRandomDatasetViaSwordApi(String dataverseToCreateDatasetIn, String apiToken) { String xmlIn = getDatasetXml(getRandomIdentifier(), getRandomIdentifier(), getRandomIdentifier()); return createDatasetViaSwordApiFromXML(dataverseToCreateDatasetIn, xmlIn, apiToken); } static Response createDatasetViaSwordApi(String dataverseToCreateDatasetIn, String title, String apiToken) { String xmlIn = getDatasetXml(title, "Lastname, Firstname", getRandomIdentifier()); return createDatasetViaSwordApiFromXML(dataverseToCreateDatasetIn, xmlIn, apiToken); } private static Response createDatasetViaSwordApiFromXML(String dataverseToCreateDatasetIn, String xmlIn, String apiToken) { Response createDatasetResponse = given() .auth().basic(apiToken, EMPTY_STRING) .body(xmlIn) .contentType("application/atom+xml") .post(swordConfiguration.getBaseUrlPathCurrent() + "/collection/dataverse/" + dataverseToCreateDatasetIn); return createDatasetResponse; } static Response listDatasetsViaSword(String dataverseAlias, String apiToken) { Response response = given() .auth().basic(apiToken, EMPTY_STRING) .get(swordConfiguration.getBaseUrlPathCurrent() + "/collection/dataverse/" + dataverseAlias); return response; } static Response updateDatasetTitleViaSword(String persistentId, String newTitle, String apiToken) { String xmlIn = getDatasetXml(newTitle, getRandomIdentifier(), getRandomIdentifier()); Response updateDatasetResponse = given() .auth().basic(apiToken, EMPTY_STRING) .body(xmlIn) .contentType("application/atom+xml") .put(swordConfiguration.getBaseUrlPathCurrent() + "/edit/study/" + persistentId); return updateDatasetResponse; } static private String getDatasetXml(String title, String author, String description) { String xmlIn = "<?xml version=\"1.0\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:dcterms=\"http://purl.org/dc/terms/\">\n" + " <dcterms:title>" + title + "</dcterms:title>\n" + " <dcterms:creator>" + author + "</dcterms:creator>\n" + " <dcterms:description>" + description + "</dcterms:description>\n" + "</entry>\n" + ""; return xmlIn; } public static Response uploadRandomFile(String persistentId, String apiToken) { String zipfilename = "trees.zip"; return uploadFile(persistentId, zipfilename, apiToken); } public static Response uploadFile(String persistentId, String zipfilename, String apiToken) { String pathToFileName = "scripts/search/data/binary/" + zipfilename; byte[] bytes = null; try { bytes = Files.readAllBytes(Paths.get(pathToFileName)); logger.info("Number of bytes to upload: " + bytes.length); } catch (IOException ex) { throw new RuntimeException("Problem getting bytes from " + pathToFileName + ": " + ex); } Response swordStatementResponse = given() .body(bytes) .header("Packaging", "http://purl.org/net/sword/package/SimpleZip") .header("Content-Disposition", "filename=" + zipfilename) /** * It's unclear why we need to add "preemptive" to auth but * without it we can't use send bytes using the body/content * method. See * https://github.com/jayway/rest-assured/issues/507#issuecomment-162963787 */ .auth().preemptive().basic(apiToken, EMPTY_STRING) .post(swordConfiguration.getBaseUrlPathCurrent() + "/edit-media/study/" + persistentId); return swordStatementResponse; } static Response downloadFile(Integer fileId) { return given() // .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/access/datafile/" + fileId); } static Response downloadFile(Integer fileId, String apiToken) { return given() /** * Data Access API does not support X-Dataverse-key header - * https://github.com/IQSS/dataverse/issues/2662 */ //.header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/access/datafile/" + fileId + "?key=" + apiToken); } static Response getSwordAtomEntry(String persistentId, String apiToken) { Response response = given() .auth().basic(apiToken, EMPTY_STRING) .get(swordConfiguration.getBaseUrlPathCurrent() + "/edit/study/" + persistentId); return response; } static Response getSwordStatement(String persistentId, String apiToken) { Response swordStatementResponse = given() .auth().basic(apiToken, EMPTY_STRING) .get(swordConfiguration.getBaseUrlPathCurrent() + "/statement/study/" + persistentId); return swordStatementResponse; } static Integer getFileIdFromSwordStatementResponse(Response swordStatement) { Integer fileId = getFileIdFromSwordStatementBody(swordStatement.body().asString()); return fileId; } private static Integer getFileIdFromSwordStatementBody(String swordStatement) { XmlPath xmlPath = new XmlPath(swordStatement); try { String fileIdAsString = xmlPath.get("feed.entry[0].id").toString().split("/")[10]; Integer fileIdAsInt = Integer.parseInt(fileIdAsString); return fileIdAsInt; } catch (IndexOutOfBoundsException ex) { return null; } } static String getFilenameFromSwordStatementResponse(Response swordStatement) { String filename = getFirstFilenameFromSwordStatementResponse(swordStatement.body().asString()); return filename; } private static String getFirstFilenameFromSwordStatementResponse(String swordStatement) { XmlPath xmlPath = new XmlPath(swordStatement); try { String filename = xmlPath.get("feed.entry[0].id").toString().split("/")[11]; return filename; } catch (IndexOutOfBoundsException ex) { return null; } } static String getTitleFromSwordStatementResponse(Response swordStatement) { return getTitleFromSwordStatement(swordStatement.getBody().asString()); } private static String getTitleFromSwordStatement(String swordStatement) { return new XmlPath(swordStatement).getString("feed.title"); } static Response createGroup(String dataverseToCreateGroupIn, String aliasInOwner, String displayName, String apiToken) { JsonObjectBuilder groupBuilder = Json.createObjectBuilder(); groupBuilder.add("aliasInOwner", aliasInOwner); groupBuilder.add("displayName", displayName); Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .body(groupBuilder.build().toString()) .contentType(ContentType.JSON) .post("/api/dataverses/" + dataverseToCreateGroupIn + "/groups"); return response; } static Response addToGroup(String dataverseThatGroupBelongsIn, String groupIdentifier, List<String> roleAssigneesToAdd, String apiToken) { JsonArrayBuilder groupBuilder = Json.createArrayBuilder(); roleAssigneesToAdd.stream().forEach((string) -> { groupBuilder.add(string); }); Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .body(groupBuilder.build().toString()) .contentType(ContentType.JSON) .post("/api/dataverses/" + dataverseThatGroupBelongsIn + "/groups/" + groupIdentifier + "/roleAssignees"); return response; } static public Response grantRoleOnDataverse(String definitionPoint, String role, String roleAssignee, String apiToken) { JsonObjectBuilder roleBuilder = Json.createObjectBuilder(); roleBuilder.add("assignee", roleAssignee); roleBuilder.add("role", role); JsonObject roleObject = roleBuilder.build(); logger.info("Granting role on dataverse alias \"" + definitionPoint + "\": " + roleObject); return given() .body(roleObject.toString()) .contentType(ContentType.JSON) .post("api/dataverses/" + definitionPoint + "/assignments?key=" + apiToken); } public static Response deleteUser(String username) { Response deleteUserResponse = given() .delete("/api/admin/authenticatedUsers/" + username + "/"); return deleteUserResponse; } public static Response deleteDataverse(String doomed, String apiToken) { return given().delete("/api/dataverses/" + doomed + "?key=" + apiToken); } public static Response deleteDatasetViaNativeApi(Integer datasetId, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("/api/datasets/" + datasetId); } public static Response deleteDatasetVersionViaNativeApi(Integer datasetId, String versionId, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("/api/datasets/" + datasetId + "/versions/" + versionId); } static Response deleteLatestDatasetVersionViaSwordApi(String persistentId, String apiToken) { return given() .auth().basic(apiToken, EMPTY_STRING) .relaxedHTTPSValidation() .delete(swordConfiguration.getBaseUrlPathCurrent() + "/edit/study/" + persistentId); } static Response destroyDataset(Integer datasetId, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("/api/datasets/" + datasetId + "/destroy"); } static Response deleteFile(Integer fileId, String apiToken) { return given() .auth().basic(apiToken, EMPTY_STRING) .relaxedHTTPSValidation() .delete(swordConfiguration.getBaseUrlPathCurrent() + "/edit-media/file/" + fileId); } static Response publishDatasetViaSword(String persistentId, String apiToken) { return given() .auth().basic(apiToken, EMPTY_STRING) .header("In-Progress", "false") .post(swordConfiguration.getBaseUrlPathCurrent() + "/edit/study/" + persistentId); } static Response publishDatasetViaNativeApi(Integer datasetId, String majorOrMinor, String apiToken) { /** * @todo This should be a POST rather than a GET: * https://github.com/IQSS/dataverse/issues/2431 * * @todo Prevent version less than v1.0 to be published (i.e. v0.1): * https://github.com/IQSS/dataverse/issues/2461 */ return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .urlEncodingEnabled(false) .get("/api/datasets/" + datasetId + "/actions/:publish?type=" + majorOrMinor); } static Response publishDataverseViaSword(String alias, String apiToken) { return given() .auth().basic(apiToken, EMPTY_STRING) .header("In-Progress", "false") .post(swordConfiguration.getBaseUrlPathCurrent() + "/edit/dataverse/" + alias); } static Response nativeGetUsingPersistentId(String persistentId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/datasets/:persistentId/?persistentId=" + persistentId); return response; } static Response makeSuperUser(String username) { Response response = given().post("/api/admin/superuser/" + username); return response; } static Response reindexDataset(String persistentId) { Response response = given() .get("/api/admin/index/dataset?persistentId=" + persistentId); return response; } static Response listAuthenticatedUsers(String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/admin/authenticatedUsers"); return response; } static Response getAuthenticatedUser(String userIdentifier, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/admin/authenticatedUsers/" + userIdentifier); return response; } static Response migrateShibToBuiltin(Long userIdToConvert, String newEmailAddress, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .body(newEmailAddress) .put("/api/admin/authenticatedUsers/id/" + userIdToConvert + "/convertShibToBuiltIn"); return response; } static Response migrateBuiltinToShib(String data, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .body(data) .put("/api/admin/authenticatedUsers/convert/builtin2shib"); return response; } static Response nativeGet(Integer datasetId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/datasets/" + datasetId); return response; } static Response privateUrlGet(Integer datasetId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/datasets/" + datasetId + "/privateUrl"); return response; } static Response privateUrlCreate(Integer datasetId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .post("/api/datasets/" + datasetId + "/privateUrl"); return response; } static Response privateUrlDelete(Integer datasetId, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("/api/datasets/" + datasetId + "/privateUrl"); return response; } static Response search(String query, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("/api/search?q=" + query); } static Response indexClear() { return given() .get("/api/admin/index/clear"); } static Response index() { return given() .get("/api/admin/index"); } static Response enableSetting(SettingsServiceBean.Key settingKey) { Response response = given().body("true").when().put("/api/admin/settings/" + settingKey); return response; } static Response deleteSetting(SettingsServiceBean.Key settingKey) { Response response = given().when().delete("/api/admin/settings/" + settingKey); return response; } static Response getSetting(SettingsServiceBean.Key settingKey) { Response response = given().when().get("/api/admin/settings/" + settingKey); return response; } static Response setSetting(SettingsServiceBean.Key settingKey, String value) { Response response = given().body(value).when().put("/api/admin/settings/" + settingKey); return response; } static Response getRoleAssignmentsOnDataverse(String dataverseAliasOrId, String apiToken) { String url = "/api/dataverses/" + dataverseAliasOrId + "/assignments"; System.out.println("URL: " + url); return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get(url); } static Response getRoleAssignmentsOnDataset(String datasetId, String persistentId, String apiToken) { String url = "/api/datasets/" + datasetId + "/assignments"; System.out.println("URL: " + url); return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get(url); } static Response grantRoleOnDataset(String definitionPoint, String role, String roleAssignee, String apiToken) { logger.info("Granting role on dataset \"" + definitionPoint + "\": " + role); return given() .body("@" + roleAssignee) .post("api/datasets/" + definitionPoint + "/assignments?key=" + apiToken); } static Response revokeRole(String definitionPoint, long doomed, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("api/dataverses/" + definitionPoint + "/assignments/" + doomed); } static Response findPermissionsOn(String dvObject, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("api/admin/permissions/" + dvObject); } static Response findRoleAssignee(String roleAssignee, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .get("api/admin/assignee/" + roleAssignee); } @Test public void testGetFileIdFromSwordStatementWithNoFiles() { String swordStatementWithNoFiles = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <id>https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit/study/doi:10.5072/FK2/0TLRLH</id>\n" + " <link href=\"https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit/study/doi:10.5072/FK2/0TLRLH\" rel=\"self\"/>\n" + " <title type=\"text\">A Dataset Without Any Files</title>\n" + " <author>\n" + " <name>Fileless, Joe</name>\n" + " </author>\n" + " <updated>2015-12-08T15:30:50.865Z</updated>\n" + " <category term=\"latestVersionState\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">DRAFT</category>\n" + " <category term=\"locked\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">false</category>\n" + " <category term=\"isMinorUpdate\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">true</category>\n" + "</feed>"; Integer fileId = getFileIdFromSwordStatementBody(swordStatementWithNoFiles); assertNull(fileId); } @Test public void testSwordStatementWithFiles() { String swordStatementWithNoFiles = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <id>https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit/study/doi:10.5072/FK2/EUEW70</id>\n" + " <link href=\"https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit/study/doi:10.5072/FK2/EUEW70\" rel=\"self\"/>\n" + " <title type=\"text\">A Dataset with a File</title>\n" + " <author>\n" + " <name>Files, John</name>\n" + " </author>\n" + " <updated>2015-12-08T15:38:29.900Z</updated>\n" + " <entry>\n" + " <content type=\"application/zip\" src=\"https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit-media/file/174/trees.zip\"/>\n" + " <id>https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit-media/file/174/trees.zip</id>\n" + " <title type=\"text\">Resource https://localhost:8080/dvn/api/data-deposit/v1.1/swordv2/edit-media/file/174/trees.zip</title>\n" + " <summary type=\"text\">Resource Part</summary>\n" + " <updated>2015-12-08T15:38:30.089Z</updated>\n" + " </entry>\n" + " <category term=\"latestVersionState\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">DRAFT</category>\n" + " <category term=\"locked\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">false</category>\n" + " <category term=\"isMinorUpdate\" scheme=\"http://purl.org/net/sword/terms/state\" label=\"State\">true</category>\n" + "</feed>"; Integer fileId = getFileIdFromSwordStatementBody(swordStatementWithNoFiles); assertNotNull(fileId); assertEquals(Integer.class, fileId.getClass()); String title = getTitleFromSwordStatement(swordStatementWithNoFiles); assertEquals("A Dataset with a File", title); } }