From e001f4860a9489ced07bfb6ab6a000ef8621371f Mon Sep 17 00:00:00 2001 From: Gregory Rushton Date: Tue, 5 Dec 2023 11:12:04 -0500 Subject: [PATCH] [DUOS-2703][risk=no] Delete study API (#2187) --- .../consent/http/ConsentApplication.java | 2 + .../consent/http/db/StudyDAO.java | 7 +- .../http/resources/DatasetResource.java | 75 ----- .../consent/http/resources/StudyResource.java | 201 ++++++++++++++ .../consent/http/service/DatasetService.java | 4 + .../service/dao/DatasetDeletionException.java | 9 + .../http/service/dao/DatasetServiceDAO.java | 30 +- .../resources/assets/paths/studyById.yaml | 26 +- .../paths/studyRegistrationByStudyId.yaml | 1 - .../consent/http/db/StudyDAOTest.java | 34 +-- .../http/resources/DatasetResourceTest.java | 136 --------- .../http/resources/StudyResourceTest.java | 260 ++++++++++++++++++ .../service/dao/DatasetServiceDAOTest.java | 26 ++ 13 files changed, 573 insertions(+), 238 deletions(-) create mode 100644 src/main/java/org/broadinstitute/consent/http/resources/StudyResource.java create mode 100644 src/main/java/org/broadinstitute/consent/http/service/dao/DatasetDeletionException.java create mode 100644 src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java diff --git a/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java b/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java index ffe5d1c387..4ab51429d4 100644 --- a/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java +++ b/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java @@ -69,6 +69,7 @@ import org.broadinstitute.consent.http.resources.SamResource; import org.broadinstitute.consent.http.resources.SchemaResource; import org.broadinstitute.consent.http.resources.StatusResource; +import org.broadinstitute.consent.http.resources.StudyResource; import org.broadinstitute.consent.http.resources.SwaggerResource; import org.broadinstitute.consent.http.resources.TDRResource; import org.broadinstitute.consent.http.resources.TosResource; @@ -246,6 +247,7 @@ public void run(ConsentConfiguration config, Environment env) { env.jersey().register( new TDRResource(tdrService, datasetService, userService, dataAccessRequestService)); env.jersey().register(new MailResource(emailService)); + env.jersey().register(injector.getInstance(StudyResource.class)); // Authentication filters final UserRoleDAO userRoleDAO = injector.getProvider(UserRoleDAO.class).get(); diff --git a/src/main/java/org/broadinstitute/consent/http/db/StudyDAO.java b/src/main/java/org/broadinstitute/consent/http/db/StudyDAO.java index 6498c59b07..3376b66969 100644 --- a/src/main/java/org/broadinstitute/consent/http/db/StudyDAO.java +++ b/src/main/java/org/broadinstitute/consent/http/db/StudyDAO.java @@ -120,8 +120,11 @@ void updateStudyProperty( ); @SqlUpdate(""" - DELETE FROM study_property WHERE study_property_id = :studyPropertyId + WITH property_deletes AS ( + DELETE from study_property where study_id = :studyId returning study_id + ) + DELETE FROM study WHERE study_id in (select study_id from property_deletes) """) - void deleteStudyPropertyById(@Bind("studyPropertyId") Integer studyPropertyId); + void deleteStudyByStudyId(@Bind("studyId") Integer studyId); } diff --git a/src/main/java/org/broadinstitute/consent/http/resources/DatasetResource.java b/src/main/java/org/broadinstitute/consent/http/resources/DatasetResource.java index 553dd39fd0..38121be2c2 100644 --- a/src/main/java/org/broadinstitute/consent/http/resources/DatasetResource.java +++ b/src/main/java/org/broadinstitute/consent/http/resources/DatasetResource.java @@ -51,7 +51,6 @@ import org.broadinstitute.consent.http.models.UserRole; import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup.AccessManagement; import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1; -import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1UpdateValidator; import org.broadinstitute.consent.http.models.dataset_registration_v1.builder.DatasetRegistrationSchemaV1Builder; import org.broadinstitute.consent.http.models.dto.DatasetDTO; import org.broadinstitute.consent.http.models.dto.DatasetPropertyDTO; @@ -193,48 +192,6 @@ public Response createDatasetRegistration( } } - @PUT - @Consumes({MediaType.MULTIPART_FORM_DATA}) - @Produces({MediaType.APPLICATION_JSON}) - @Path("/study/{studyId}") - @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) - /* - * This endpoint accepts a json instance of a dataset-registration-schema_v1.json schema. - * With that object, we can fully update the study/datasets from the provided values. - */ - public Response updateStudyByRegistration( - @Auth AuthUser authUser, - FormDataMultiPart multipart, - @PathParam("studyId") Integer studyId, - @FormDataParam("dataset") String json) { - try { - User user = userService.findUserByEmail(authUser.getEmail()); - Study existingStudy = datasetRegistrationService.findStudyById(studyId); - - // Manually validate the schema from an editing context. Validation with the schema tools - // enforces it in a creation context but doesn't work for editing purposes. - DatasetRegistrationSchemaV1UpdateValidator updateValidator = new DatasetRegistrationSchemaV1UpdateValidator(); - Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); - DatasetRegistrationSchemaV1 registration = gson.fromJson(json, - DatasetRegistrationSchemaV1.class); - - if (updateValidator.validate(existingStudy, registration)) { - // Update study from registration - Map files = extractFilesFromMultiPart(multipart); - Study updatedStudy = datasetRegistrationService.updateStudyFromRegistration( - studyId, - registration, - user, - files); - return Response.ok(updatedStudy).build(); - } else { - return Response.status(Status.BAD_REQUEST).build(); - } - } catch (Exception e) { - return createExceptionResponse(e); - } - } - /** * Finds and validates all the files uploaded to the multipart. * @@ -411,38 +368,6 @@ public Response getDataset(@PathParam("datasetId") Integer datasetId) { } } - @GET - @Path("/study/{studyId}") - @Produces(MediaType.APPLICATION_JSON) - @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) - public Response getStudyById(@PathParam("studyId") Integer studyId) { - try { - Study study = datasetService.getStudyWithDatasetsById(studyId); - return Response.ok(study).build(); - } catch (Exception e) { - return createExceptionResponse(e); - } - } - - @GET - @Path("/study/registration/{studyId}") - @Produces(MediaType.APPLICATION_JSON) - @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) - public Response getRegistrationFromStudy(@Auth AuthUser authUser, - @PathParam("studyId") Integer studyId) { - try { - Study study = datasetService.getStudyWithDatasetsById(studyId); - List datasets = - Objects.nonNull(study.getDatasets()) ? study.getDatasets().stream().toList() : List.of(); - DatasetRegistrationSchemaV1 registration = new DatasetRegistrationSchemaV1Builder().build( - study, datasets); - String entity = GsonUtil.buildGsonNullSerializer().toJson(registration); - return Response.ok().entity(entity).build(); - } catch (Exception e) { - return createExceptionResponse(e); - } - } - @GET @Path("/registration/{datasetIdentifier}") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/broadinstitute/consent/http/resources/StudyResource.java b/src/main/java/org/broadinstitute/consent/http/resources/StudyResource.java new file mode 100644 index 0000000000..25440fc771 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/resources/StudyResource.java @@ -0,0 +1,201 @@ +package org.broadinstitute.consent.http.resources; + +import com.google.gson.Gson; +import com.google.inject.Inject; +import io.dropwizard.auth.Auth; +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.broadinstitute.consent.http.enumeration.UserRoles; +import org.broadinstitute.consent.http.models.AuthUser; +import org.broadinstitute.consent.http.models.Dataset; +import org.broadinstitute.consent.http.models.Study; +import org.broadinstitute.consent.http.models.User; +import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1; +import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1UpdateValidator; +import org.broadinstitute.consent.http.models.dataset_registration_v1.builder.DatasetRegistrationSchemaV1Builder; +import org.broadinstitute.consent.http.service.DatasetRegistrationService; +import org.broadinstitute.consent.http.service.DatasetService; +import org.broadinstitute.consent.http.service.ElasticSearchService; +import org.broadinstitute.consent.http.service.UserService; +import org.broadinstitute.consent.http.util.gson.GsonUtil; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.FormDataParam; + +@Path("api/dataset/study") +public class StudyResource extends Resource { + + private final DatasetService datasetService; + private final DatasetRegistrationService datasetRegistrationService; + private final UserService userService; + private final ElasticSearchService elasticSearchService; + + + @Inject + public StudyResource(DatasetService datasetService, UserService userService, + DatasetRegistrationService datasetRegistrationService, + ElasticSearchService elasticSearchService) { + this.datasetService = datasetService; + this.userService = userService; + this.datasetRegistrationService = datasetRegistrationService; + this.elasticSearchService = elasticSearchService; + } + + @GET + @Path("/{studyId}") + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) + public Response getStudyById(@PathParam("studyId") Integer studyId) { + try { + Study study = datasetService.getStudyWithDatasetsById(studyId); + return Response.ok(study).build(); + } catch (Exception e) { + return createExceptionResponse(e); + } + } + + @DELETE + @Path("/{studyId}") + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) + public Response deleteStudyById(@Auth AuthUser authUser, @PathParam("studyId") Integer studyId) { + try { + User user = userService.findUserByEmail(authUser.getEmail()); + Study study = datasetService.getStudyWithDatasetsById(studyId); + + if (Objects.isNull(study)) { + throw new NotFoundException("Study not found"); + } + + // If the user is not an admin, ensure that they are the study/dataset creator + if (!user.hasUserRole(UserRoles.ADMIN) && (!Objects.equals(study.getCreateUserId(), + user.getUserId()))) { + throw new NotFoundException("Study not found"); + } + + boolean deletable = study.getDatasets() + .stream() + .allMatch(Dataset::getDeletable); + if (!deletable) { + throw new BadRequestException("Study has datasets that are in use and cannot be deleted."); + } + Set studyDatasetIds = study.getDatasetIds(); + datasetService.deleteStudy(study, user); + // Remove from ES index + studyDatasetIds.forEach(id -> { + try { + elasticSearchService.deleteIndex(id); + } catch (IOException e) { + logException(e); + } + }); + return Response.ok().build(); + } catch (Exception e) { + return createExceptionResponse(e); + } + } + + @GET + @Path("/registration/{studyId}") + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) + public Response getRegistrationFromStudy(@Auth AuthUser authUser, + @PathParam("studyId") Integer studyId) { + try { + Study study = datasetService.getStudyWithDatasetsById(studyId); + List datasets = + Objects.nonNull(study.getDatasets()) ? study.getDatasets().stream().toList() : List.of(); + DatasetRegistrationSchemaV1 registration = new DatasetRegistrationSchemaV1Builder().build( + study, datasets); + String entity = GsonUtil.buildGsonNullSerializer().toJson(registration); + return Response.ok().entity(entity).build(); + } catch (Exception e) { + return createExceptionResponse(e); + } + } + + @PUT + @Consumes({MediaType.MULTIPART_FORM_DATA}) + @Produces({MediaType.APPLICATION_JSON}) + @Path("/{studyId}") + @RolesAllowed({ADMIN, CHAIRPERSON, DATASUBMITTER}) + /* + * This endpoint accepts a json instance of a dataset-registration-schema_v1.json schema. + * With that object, we can fully update the study/datasets from the provided values. + */ + public Response updateStudyByRegistration( + @Auth AuthUser authUser, + FormDataMultiPart multipart, + @PathParam("studyId") Integer studyId, + @FormDataParam("dataset") String json) { + try { + User user = userService.findUserByEmail(authUser.getEmail()); + Study existingStudy = datasetRegistrationService.findStudyById(studyId); + + // Manually validate the schema from an editing context. Validation with the schema tools + // enforces it in a creation context but doesn't work for editing purposes. + DatasetRegistrationSchemaV1UpdateValidator updateValidator = new DatasetRegistrationSchemaV1UpdateValidator(); + Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); + DatasetRegistrationSchemaV1 registration = gson.fromJson(json, + DatasetRegistrationSchemaV1.class); + + if (updateValidator.validate(existingStudy, registration)) { + // Update study from registration + Map files = extractFilesFromMultiPart(multipart); + Study updatedStudy = datasetRegistrationService.updateStudyFromRegistration( + studyId, + registration, + user, + files); + return Response.ok(updatedStudy).build(); + } else { + return Response.status(Status.BAD_REQUEST).build(); + } + } catch (Exception e) { + return createExceptionResponse(e); + } + } + + /** + * Finds and validates all the files uploaded to the multipart. + * + * @param multipart Form data + * @return Map of file body parts, where the key is the name of the field and the value is the + * body part including the file(s). + */ + private Map extractFilesFromMultiPart(FormDataMultiPart multipart) { + if (Objects.isNull(multipart)) { + return Map.of(); + } + + Map files = new HashMap<>(); + for (List parts : multipart.getFields().values()) { + for (FormDataBodyPart part : parts) { + if (Objects.nonNull(part.getContentDisposition().getFileName())) { + validateFileDetails(part.getContentDisposition()); + files.put(part.getName(), part); + } + } + } + + return files; + } + +} diff --git a/src/main/java/org/broadinstitute/consent/http/service/DatasetService.java b/src/main/java/org/broadinstitute/consent/http/service/DatasetService.java index 1e42a4c955..cb5f17fb10 100644 --- a/src/main/java/org/broadinstitute/consent/http/service/DatasetService.java +++ b/src/main/java/org/broadinstitute/consent/http/service/DatasetService.java @@ -293,6 +293,10 @@ public void deleteDataset(Integer datasetId, Integer userId) throws Exception { } } + public void deleteStudy(Study study, User user) throws Exception { + datasetServiceDAO.deleteStudy(study, user); + } + public List searchDatasets(String query, AccessManagement accessManagement, User user) { List datasets = findAllDatasetsByUser(user); return datasets.stream().filter(ds -> ds.isDatasetMatch(query, accessManagement)).toList(); diff --git a/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetDeletionException.java b/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetDeletionException.java new file mode 100644 index 0000000000..0cb5e8e6bf --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetDeletionException.java @@ -0,0 +1,9 @@ +package org.broadinstitute.consent.http.service.dao; + +public class DatasetDeletionException extends RuntimeException { + + public DatasetDeletionException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAO.java b/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAO.java index 2e23c4a3d0..8dc715b96b 100644 --- a/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAO.java +++ b/src/main/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAO.java @@ -24,6 +24,7 @@ import org.broadinstitute.consent.http.models.FileStorageObject; import org.broadinstitute.consent.http.models.Study; import org.broadinstitute.consent.http.models.StudyProperty; +import org.broadinstitute.consent.http.models.User; import org.broadinstitute.consent.http.util.ConsentLogger; import org.jdbi.v3.core.Handle; import org.jdbi.v3.core.Jdbi; @@ -65,6 +66,29 @@ public void deleteDataset(Dataset dataset, Integer userId) throws Exception { }); } + public void deleteStudy(Study study, User user) throws Exception { + jdbi.useHandle(handle -> { + handle.getConnection().setAutoCommit(false); + study.getDatasets().forEach(d -> { + try { + deleteDataset(d, user.getUserId()); + } catch (Exception e) { + handle.rollback(); + logException(e); + throw new DatasetDeletionException(e); + } + }); + try { + studyDAO.deleteStudyByStudyId(study.getStudyId()); + } catch (Exception e) { + handle.rollback(); + logException(e); + throw e; + } + handle.commit(); + }); + } + public record StudyInsert(String name, String description, List dataTypes, @@ -217,9 +241,9 @@ private Integer executeInsertStudy(Handle handle, StudyInsert insert) { public Study updateStudy(StudyUpdate studyUpdate, List datasetUpdates, List datasetInserts) throws SQLException { jdbi.useHandle( - handle -> { - handle.getConnection().setAutoCommit(false); - executeUpdateStudy(handle, studyUpdate); + handle -> { + handle.getConnection().setAutoCommit(false); + executeUpdateStudy(handle, studyUpdate); for (DatasetUpdate datasetUpdate : datasetUpdates) { executeUpdateDatasetWithFiles( handle, diff --git a/src/main/resources/assets/paths/studyById.yaml b/src/main/resources/assets/paths/studyById.yaml index b5aadab1b9..58668ccb4e 100644 --- a/src/main/resources/assets/paths/studyById.yaml +++ b/src/main/resources/assets/paths/studyById.yaml @@ -9,7 +9,6 @@ get: schema: type: integer tags: - - Dataset - Study responses: 200: @@ -26,7 +25,6 @@ put: summary: Update Dataset Registration By Study Id description: Updates Dataset Registration specified by Study Id. tags: - - Dataset - Study parameters: - name: studyId @@ -81,3 +79,27 @@ put: description: Not Found 500: description: Server error +delete: + summary: Delete Study + description: | + Deletes Study, Datasets, and all properties of the Study specified by id. + * Admins can delete any study + * Chairpersons and Data Submitters can delete studies they created + parameters: + - name: studyId + in: path + description: Study ID + required: true + schema: + type: integer + tags: + - Study + responses: + 200: + description: The Study was successfully deleted + 400: + description: Study is in use and cannot be deleted + 404: + description: Not Found + 500: + description: Server error diff --git a/src/main/resources/assets/paths/studyRegistrationByStudyId.yaml b/src/main/resources/assets/paths/studyRegistrationByStudyId.yaml index d1a49f67ad..0430157114 100644 --- a/src/main/resources/assets/paths/studyRegistrationByStudyId.yaml +++ b/src/main/resources/assets/paths/studyRegistrationByStudyId.yaml @@ -9,7 +9,6 @@ get: schema: type: integer tags: - - Dataset - Study responses: 200: diff --git a/src/test/java/org/broadinstitute/consent/http/db/StudyDAOTest.java b/src/test/java/org/broadinstitute/consent/http/db/StudyDAOTest.java index 5602466e86..24679c3fa3 100644 --- a/src/test/java/org/broadinstitute/consent/http/db/StudyDAOTest.java +++ b/src/test/java/org/broadinstitute/consent/http/db/StudyDAOTest.java @@ -1,7 +1,6 @@ package org.broadinstitute.consent.http.db; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,10 +25,10 @@ import org.broadinstitute.consent.http.models.User; import org.junit.jupiter.api.Test; -public class StudyDAOTest extends DAOTestHelper { +class StudyDAOTest extends DAOTestHelper { @Test - public void testCreateAndFindStudy() { + void testCreateAndFindStudy() { User u = createUser(); String name = RandomStringUtils.randomAlphabetic(20); @@ -79,7 +78,7 @@ public void testCreateAndFindStudy() { } @Test - public void testStudyProps() { + void testStudyProps() { User u = createUser(); String name = RandomStringUtils.randomAlphabetic(20); @@ -157,7 +156,7 @@ public void testStudyProps() { @Test - public void testAlternativeDataSharingPlan() { + void testAlternativeDataSharingPlan() { User u = createUser(); UUID uuid = UUID.randomUUID(); @@ -184,7 +183,7 @@ public void testAlternativeDataSharingPlan() { } @Test - public void testGetAlternativeDataSharingFile() { + void testGetAlternativeDataSharingFile() { Study study = insertStudyWithProperties(); // create unrelated file with the same id as dataset id but different category, timestamp before @@ -214,7 +213,7 @@ public void testGetAlternativeDataSharingFile() { } @Test - public void testGetAlternativeDataSharingPlanFile_AlwaysLatestCreated() { + void testGetAlternativeDataSharingPlanFile_AlwaysLatestCreated() { Study study = insertStudyWithProperties(); String fileName = org.apache.commons.lang3.RandomStringUtils.randomAlphabetic(10); @@ -259,7 +258,7 @@ public void testGetAlternativeDataSharingPlanFile_AlwaysLatestCreated() { } @Test - public void testGetAlternativeDataSharingPlanFile_NotDeleted() { + void testGetAlternativeDataSharingPlanFile_NotDeleted() { Study study = insertStudyWithProperties(); FileStorageObject altFile = createFileStorageObject( @@ -281,7 +280,7 @@ public void testGetAlternativeDataSharingPlanFile_NotDeleted() { } @Test - public void testIncludesDatasetIds() { + void testIncludesDatasetIds() { Study s = insertStudyWithProperties(); insertDataset(); @@ -298,7 +297,7 @@ public void testIncludesDatasetIds() { } @Test - public void testUpdateStudy() { + void testUpdateStudy() { User user = createUser(); Study study = insertStudyWithProperties(); String newName = "New Name"; @@ -326,7 +325,7 @@ public void testUpdateStudy() { } @Test - public void testUpdateStudyProperty() { + void testUpdateStudyProperty() { Study study = insertStudyWithProperties(); String newPropStringVal = RandomStringUtils.randomAlphabetic(15); Integer newPropNumberVal = RandomUtils.nextInt(100, 1000); @@ -355,16 +354,13 @@ public void testUpdateStudyProperty() { } @Test - public void testDeleteStudyProperty() { + void testDeleteStudyById() { Study study = insertStudyWithProperties(); - assertNotNull(study.getProperties()); - assertFalse(study.getProperties().isEmpty()); + Integer id = study.getStudyId(); - study.getProperties().forEach(p -> { - studyDAO.deleteStudyPropertyById(p.getStudyPropertyId()); - }); - Study updatedStudy = studyDAO.findStudyById(study.getStudyId()); - assertNull(updatedStudy.getProperties()); + studyDAO.deleteStudyByStudyId(id); + Study deletedStudy = studyDAO.findStudyById(id); + assertNull(deletedStudy); } private FileStorageObject createFileStorageObject(String entityId, FileCategory category) { diff --git a/src/test/java/org/broadinstitute/consent/http/resources/DatasetResourceTest.java b/src/test/java/org/broadinstitute/consent/http/resources/DatasetResourceTest.java index 3f59d33f61..981e0b8118 100644 --- a/src/test/java/org/broadinstitute/consent/http/resources/DatasetResourceTest.java +++ b/src/test/java/org/broadinstitute/consent/http/resources/DatasetResourceTest.java @@ -28,13 +28,10 @@ import java.net.URI; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.broadinstitute.consent.http.authentication.GenericUser; @@ -45,13 +42,11 @@ import org.broadinstitute.consent.http.models.DataUseBuilder; import org.broadinstitute.consent.http.models.Dataset; import org.broadinstitute.consent.http.models.DatasetProperty; -import org.broadinstitute.consent.http.models.Dictionary; import org.broadinstitute.consent.http.models.Error; import org.broadinstitute.consent.http.models.Study; import org.broadinstitute.consent.http.models.StudyProperty; import org.broadinstitute.consent.http.models.User; import org.broadinstitute.consent.http.models.UserRole; -import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup; import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup.AccessManagement; import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup.DataLocation; import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1; @@ -68,8 +63,6 @@ import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; class DatasetResourceTest { @@ -103,9 +96,6 @@ class DatasetResourceTest { @Mock private UriBuilder uriBuilder; - @Mock - private Collection dictionaries; - private DatasetResource resource; @BeforeEach @@ -973,83 +963,6 @@ void testCreateDatasetRegistration_invalidFileName() { assertEquals(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, response.getStatus()); } - @Test - void testGetStudyByIdNoDatasets() { - Study study = new Study(); - study.setStudyId(1); - study.setName("asdfasdfasdfasdfasdfasdf"); - when(datasetService.getStudyWithDatasetsById(1)).thenReturn(study); - initResource(); - Response response = resource.getStudyById(1); - assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); - } - - @Test - void testGetStudyByIdWithDatasets() { - Dataset ds1 = new Dataset(); - ds1.setDataSetId(1); - Dataset ds2 = new Dataset(); - ds2.setDataSetId(2); - Dataset ds3 = new Dataset(); - ds3.setDataSetId(3); - List datasets = List.of(ds1, ds2, ds3); - - Study study = new Study(); - study.setName(RandomStringUtils.randomAlphabetic(10)); - study.setStudyId(12345); - study.setDatasetIds(Set.of(1, 2, 3)); - - List datasetIds = new ArrayList<>(study.getDatasetIds()); - - when(datasetService.getStudyWithDatasetsById(12345)).thenReturn(study); - when(datasetService.findDatasetsByIds(datasetIds)).thenReturn(datasets); - - initResource(); - Response response = resource.getStudyById(12345); - assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); - assertEquals(study.getDatasetIds().size(), datasets.size()); - } - - @Test - void testGetStudyByIdNotFound() { - when(datasetService.getStudyWithDatasetsById(1)).thenThrow(new NotFoundException()); - - initResource(); - Response response = resource.getStudyById(1); - assertEquals(HttpStatusCodes.STATUS_CODE_NOT_FOUND, response.getStatus()); - } - - @Test - void testGetRegistrationFromStudy() { - Study study = createMockStudy(); - when(datasetService.getStudyWithDatasetsById(any())).thenReturn(study); - - initResource(); - Response response = resource.getRegistrationFromStudy(authUser, 1); - assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); - } - - @Test - void testGetRegistrationFromStudyNoDatasets() { - Study study = createMockStudy(); - study.getDatasets().clear(); - when(datasetService.getStudyWithDatasetsById(any())).thenReturn(study); - - initResource(); - Response response = resource.getRegistrationFromStudy(authUser, 1); - assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); - } - - @Test - void testGetRegistrationFromStudyNotFound() { - Study study = createMockStudy(); - when(datasetService.getStudyWithDatasetsById(any())).thenThrow(new NotFoundException()); - - initResource(); - Response response = resource.getRegistrationFromStudy(authUser, study.getStudyId()); - assertEquals(HttpStatusCodes.STATUS_CODE_NOT_FOUND, response.getStatus()); - } - @Test void testGetRegistrationFromDatasetIdentifier() { Study study = createMockStudy(); @@ -1247,55 +1160,6 @@ void testSyncDataUseTranslationNotFound() { assertEquals(HttpStatusCodes.STATUS_CODE_NOT_FOUND, response.getStatus()); } - @ParameterizedTest - @ValueSource(strings = { - DataResourceTestData.registrationWithMalformedJson, - DataResourceTestData.registrationWithStudyName, - DataResourceTestData.registrationWithDataSubmitterUserId, - DataResourceTestData.registrationWithExistingCGDataUse, - DataResourceTestData.registrationWithExistingCG - }) - void testUpdateStudyByRegistrationInvalid(String input) { - Study study = createMockStudy(); - // for DataResourceTestData.registrationWithExistingCG, manipulate the dataset ids to simulate - // a dataset deletion - if (input.equals(DataResourceTestData.registrationWithExistingCG)) { - Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); - DatasetRegistrationSchemaV1 schemaV1 = gson.fromJson(input, - DatasetRegistrationSchemaV1.class); - List datasetIds = schemaV1.getConsentGroups().stream() - .map(ConsentGroup::getDatasetId).toList(); - study.setDatasetIds(Set.of(datasetIds.get(0) + 1)); - } - when(userService.findUserByEmail(any())).thenReturn(user); - when(datasetRegistrationService.findStudyById(any())).thenReturn(study); - initResource(); - - Response response = resource.updateStudyByRegistration(authUser, null, 1, input); - assertEquals(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, response.getStatus()); - } - - @Test - void testUpdateStudyByRegistration() { - String input = DataResourceTestData.validRegistration; - Study study = createMockStudy(); - Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); - DatasetRegistrationSchemaV1 schemaV1 = gson.fromJson(input, DatasetRegistrationSchemaV1.class); - Set datasetIds = schemaV1 - .getConsentGroups() - .stream() - .map(ConsentGroup::getDatasetId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - study.setDatasetIds(datasetIds); - when(userService.findUserByEmail(any())).thenReturn(user); - when(datasetRegistrationService.findStudyById(any())).thenReturn(study); - initResource(); - - Response response = resource.updateStudyByRegistration(authUser, null, 1, input); - assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); - } - /** * Helper method to create a minimally valid instance of a dataset registration schema * diff --git a/src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java b/src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java new file mode 100644 index 0000000000..7e25871e17 --- /dev/null +++ b/src/test/java/org/broadinstitute/consent/http/resources/StudyResourceTest.java @@ -0,0 +1,260 @@ +package org.broadinstitute.consent.http.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import com.google.api.client.http.HttpStatusCodes; +import com.google.gson.Gson; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; +import org.broadinstitute.consent.http.enumeration.PropertyType; +import org.broadinstitute.consent.http.models.AuthUser; +import org.broadinstitute.consent.http.models.DataUse; +import org.broadinstitute.consent.http.models.Dataset; +import org.broadinstitute.consent.http.models.DatasetProperty; +import org.broadinstitute.consent.http.models.Study; +import org.broadinstitute.consent.http.models.StudyProperty; +import org.broadinstitute.consent.http.models.User; +import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup; +import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup.AccessManagement; +import org.broadinstitute.consent.http.models.dataset_registration_v1.ConsentGroup.DataLocation; +import org.broadinstitute.consent.http.models.dataset_registration_v1.DatasetRegistrationSchemaV1; +import org.broadinstitute.consent.http.service.DatasetRegistrationService; +import org.broadinstitute.consent.http.service.DatasetService; +import org.broadinstitute.consent.http.service.ElasticSearchService; +import org.broadinstitute.consent.http.service.UserService; +import org.broadinstitute.consent.http.util.gson.GsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; + +class StudyResourceTest { + + @Mock + private DatasetService datasetService; + + @Mock + private DatasetRegistrationService datasetRegistrationService; + + @Mock + private UserService userService; + + @Mock + private ElasticSearchService elasticSearchService; + + @Mock + private AuthUser authUser; + + @Mock + private User user; + + private StudyResource resource; + + @BeforeEach + public void setUp() { + openMocks(this); + } + + private void initResource() { + resource = new StudyResource(datasetService, userService, datasetRegistrationService, elasticSearchService); + } + + @Test + void testGetStudyByIdNoDatasets() { + Study study = new Study(); + study.setStudyId(1); + study.setName("asdfasdfasdfasdfasdfasdf"); + when(datasetService.getStudyWithDatasetsById(1)).thenReturn(study); + initResource(); + Response response = resource.getStudyById(1); + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); + } + + @Test + void testGetStudyByIdWithDatasets() { + Dataset ds1 = new Dataset(); + ds1.setDataSetId(1); + Dataset ds2 = new Dataset(); + ds2.setDataSetId(2); + Dataset ds3 = new Dataset(); + ds3.setDataSetId(3); + List datasets = List.of(ds1, ds2, ds3); + + Study study = new Study(); + study.setName(RandomStringUtils.randomAlphabetic(10)); + study.setStudyId(12345); + study.setDatasetIds(Set.of(1, 2, 3)); + + List datasetIds = new ArrayList<>(study.getDatasetIds()); + + when(datasetService.getStudyWithDatasetsById(12345)).thenReturn(study); + when(datasetService.findDatasetsByIds(datasetIds)).thenReturn(datasets); + + initResource(); + Response response = resource.getStudyById(12345); + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); + assertEquals(study.getDatasetIds().size(), datasets.size()); + } + + @Test + void testGetStudyByIdNotFound() { + when(datasetService.getStudyWithDatasetsById(1)).thenThrow(new NotFoundException()); + + initResource(); + Response response = resource.getStudyById(1); + assertEquals(HttpStatusCodes.STATUS_CODE_NOT_FOUND, response.getStatus()); + } + + @Test + void testGetRegistrationFromStudy() { + Study study = createMockStudy(); + when(datasetService.getStudyWithDatasetsById(any())).thenReturn(study); + + initResource(); + Response response = resource.getRegistrationFromStudy(authUser, 1); + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); + } + + @Test + void testGetRegistrationFromStudyNoDatasets() { + Study study = createMockStudy(); + study.getDatasets().clear(); + when(datasetService.getStudyWithDatasetsById(any())).thenReturn(study); + + initResource(); + Response response = resource.getRegistrationFromStudy(authUser, 1); + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); + } + + @Test + void testGetRegistrationFromStudyNotFound() { + Study study = createMockStudy(); + when(datasetService.getStudyWithDatasetsById(any())).thenThrow(new NotFoundException()); + + initResource(); + Response response = resource.getRegistrationFromStudy(authUser, study.getStudyId()); + assertEquals(HttpStatusCodes.STATUS_CODE_NOT_FOUND, response.getStatus()); + } + + @ParameterizedTest + @ValueSource(strings = { + DataResourceTestData.registrationWithMalformedJson, + DataResourceTestData.registrationWithStudyName, + DataResourceTestData.registrationWithDataSubmitterUserId, + DataResourceTestData.registrationWithExistingCGDataUse, + DataResourceTestData.registrationWithExistingCG + }) + void testUpdateStudyByRegistrationInvalid(String input) { + Study study = createMockStudy(); + // for DataResourceTestData.registrationWithExistingCG, manipulate the dataset ids to simulate + // a dataset deletion + if (input.equals(DataResourceTestData.registrationWithExistingCG)) { + Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); + DatasetRegistrationSchemaV1 schemaV1 = gson.fromJson(input, + DatasetRegistrationSchemaV1.class); + List datasetIds = schemaV1.getConsentGroups().stream() + .map(ConsentGroup::getDatasetId).toList(); + study.setDatasetIds(Set.of(datasetIds.get(0) + 1)); + } + when(userService.findUserByEmail(any())).thenReturn(user); + when(datasetRegistrationService.findStudyById(any())).thenReturn(study); + initResource(); + + Response response = resource.updateStudyByRegistration(authUser, null, 1, input); + assertEquals(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, response.getStatus()); + } + + @Test + void testUpdateStudyByRegistration() { + String input = DataResourceTestData.validRegistration; + Study study = createMockStudy(); + Gson gson = GsonUtil.gsonBuilderWithAdapters().create(); + DatasetRegistrationSchemaV1 schemaV1 = gson.fromJson(input, DatasetRegistrationSchemaV1.class); + Set datasetIds = schemaV1 + .getConsentGroups() + .stream() + .map(ConsentGroup::getDatasetId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + study.setDatasetIds(datasetIds); + when(userService.findUserByEmail(any())).thenReturn(user); + when(datasetRegistrationService.findStudyById(any())).thenReturn(study); + initResource(); + + Response response = resource.updateStudyByRegistration(authUser, null, 1, input); + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatus()); + } + + /* + * Study mock + */ + private Study createMockStudy() { + Dataset dataset = new Dataset(); + dataset.setDataSetId(100); + dataset.setAlias(10); + dataset.setDatasetIdentifier(); + dataset.setDacId(1); + dataset.setDataUse(new DataUse()); + + Study study = new Study(); + study.setName(RandomStringUtils.randomAlphabetic(10)); + study.setDescription(RandomStringUtils.randomAlphabetic(20)); + study.setStudyId(12345); + study.setPiName(RandomStringUtils.randomAlphabetic(10)); + study.setDataTypes(List.of(RandomStringUtils.randomAlphabetic(10))); + study.setCreateUserId(9); + study.setCreateUserEmail(RandomStringUtils.randomAlphabetic(10)); + study.setPublicVisibility(true); + study.setDatasetIds(Set.of(dataset.getDataSetId())); + + StudyProperty phenotypeProperty = new StudyProperty(); + phenotypeProperty.setKey("phenotypeIndication"); + phenotypeProperty.setType(PropertyType.String); + phenotypeProperty.setValue(RandomStringUtils.randomAlphabetic(10)); + + StudyProperty speciesProperty = new StudyProperty(); + speciesProperty.setKey("species"); + speciesProperty.setType(PropertyType.String); + speciesProperty.setValue(RandomStringUtils.randomAlphabetic(10)); + + StudyProperty dataCustodianEmailProperty = new StudyProperty(); + dataCustodianEmailProperty.setKey("dataCustodianEmail"); + dataCustodianEmailProperty.setType(PropertyType.Json); + dataCustodianEmailProperty.setValue(List.of(RandomStringUtils.randomAlphabetic(10))); + + study.setProperties(Set.of(phenotypeProperty, speciesProperty, dataCustodianEmailProperty)); + + dataset.setStudy(study); + + DatasetProperty accessManagementProp = new DatasetProperty(); + accessManagementProp.setSchemaProperty("accessManagement"); + accessManagementProp.setPropertyType(PropertyType.String); + accessManagementProp.setPropertyValue(AccessManagement.OPEN.value()); + + DatasetProperty dataLocationProp = new DatasetProperty(); + dataLocationProp.setSchemaProperty("dataLocation"); + dataLocationProp.setPropertyType(PropertyType.String); + dataLocationProp.setPropertyValue(DataLocation.NOT_DETERMINED.value()); + + DatasetProperty numParticipantsProp = new DatasetProperty(); + numParticipantsProp.setSchemaProperty("numberOfParticipants"); + numParticipantsProp.setPropertyType(PropertyType.Number); + numParticipantsProp.setPropertyValue(20); + + dataset.setProperties(Set.of(accessManagementProp, dataLocationProp, numParticipantsProp)); + study.addDatasets(List.of(dataset)); + + return study; + } + +} diff --git a/src/test/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAOTest.java b/src/test/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAOTest.java index 534ef3f56c..b31f493be8 100644 --- a/src/test/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAOTest.java +++ b/src/test/java/org/broadinstitute/consent/http/service/dao/DatasetServiceDAOTest.java @@ -612,6 +612,32 @@ void testUpdateStudyWithFileUpdates() throws Exception { } + @Test + void testDeleteStudy() throws Exception { + FileStorageObject fso1 = new FileStorageObject(); + fso1.setMediaType(RandomStringUtils.randomAlphabetic(20)); + fso1.setCategory(FileCategory.ALTERNATIVE_DATA_SHARING_PLAN); + fso1.setBlobId( + BlobId.of(RandomStringUtils.randomAlphabetic(10), RandomStringUtils.randomAlphabetic(10))); + fso1.setFileName(RandomStringUtils.randomAlphabetic(10)); + + FileStorageObject fso2 = new FileStorageObject(); + fso2.setMediaType(RandomStringUtils.randomAlphabetic(20)); + fso2.setCategory(FileCategory.NIH_INSTITUTIONAL_CERTIFICATION); + fso2.setBlobId( + BlobId.of(RandomStringUtils.randomAlphabetic(10), RandomStringUtils.randomAlphabetic(10))); + fso2.setFileName(RandomStringUtils.randomAlphabetic(10)); + Study study = createStudy(List.of(fso1, fso2)); + + List datasets = datasetDAO.findDatasetsByIdList(new ArrayList<>(study.getDatasetIds())); + study.addDatasets(datasets); + + serviceDAO.deleteStudy(study, createUser()); + Study deletedStudy = studyDAO.findStudyById(study.getStudyId()); + assertNull(deletedStudy); + } + + /** * Helper method to create a study with two props and one dataset * @param fso Optional FSO to use as part of the study insert