diff --git a/src/main/java/org/breedinginsight/api/model/v1/request/ExperimentCollaboratorRequest.java b/src/main/java/org/breedinginsight/api/model/v1/request/ExperimentCollaboratorRequest.java new file mode 100644 index 000000000..b570be5f5 --- /dev/null +++ b/src/main/java/org/breedinginsight/api/model/v1/request/ExperimentCollaboratorRequest.java @@ -0,0 +1,19 @@ +package org.breedinginsight.api.model.v1.request; + +import io.micronaut.core.annotation.Introspected; +import lombok.*; + +import javax.validation.constraints.NotBlank; +import java.util.UUID; + +@Getter +@Setter +@Builder +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Introspected +public class ExperimentCollaboratorRequest { + @NotBlank + private UUID userId; +} diff --git a/src/main/java/org/breedinginsight/api/model/v1/response/ExperimentalCollaboratorResponse.java b/src/main/java/org/breedinginsight/api/model/v1/response/ExperimentalCollaboratorResponse.java new file mode 100644 index 000000000..dc0bfc5c2 --- /dev/null +++ b/src/main/java/org/breedinginsight/api/model/v1/response/ExperimentalCollaboratorResponse.java @@ -0,0 +1,44 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.api.model.v1.response; + +import lombok.*; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.UUID; + +@Getter +@Setter +@Accessors(chain=true) +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class ExperimentalCollaboratorResponse { + //collaboratorId is null in case where active=false + private UUID collaboratorId; + @NotNull + private Boolean active; + @NotBlank + private String name; + @NotBlank + private String email; + @NotBlank + private UUID userId; +} diff --git a/src/main/java/org/breedinginsight/api/v1/controller/ExperimentController.java b/src/main/java/org/breedinginsight/api/v1/controller/ExperimentController.java index 55b63e24e..46c32a871 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/ExperimentController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/ExperimentController.java @@ -11,23 +11,35 @@ import lombok.extern.slf4j.Slf4j; import org.brapi.client.v2.model.exceptions.ApiException; import org.breedinginsight.api.auth.*; +import org.breedinginsight.api.model.v1.request.ExperimentCollaboratorRequest; import org.breedinginsight.api.model.v1.request.SubEntityDatasetRequest; +import org.breedinginsight.api.model.v1.response.DataResponse; +import org.breedinginsight.api.model.v1.response.ExperimentalCollaboratorResponse; import org.breedinginsight.api.model.v1.response.Response; +import org.breedinginsight.api.model.v1.response.metadata.Metadata; +import org.breedinginsight.api.model.v1.response.metadata.Pagination; +import org.breedinginsight.api.model.v1.response.metadata.Status; +import org.breedinginsight.api.model.v1.response.metadata.StatusCode; import org.breedinginsight.brapi.v2.model.request.query.ExperimentExportQuery; import org.breedinginsight.brapi.v2.services.BrAPITrialService; -import org.breedinginsight.model.Dataset; -import org.breedinginsight.model.DatasetMetadata; -import org.breedinginsight.model.DownloadFile; -import org.breedinginsight.model.Program; +import org.breedinginsight.dao.db.tables.pojos.ExperimentProgramUserRoleEntity; +import org.breedinginsight.model.*; +import org.breedinginsight.services.ExperimentalCollaboratorService; import org.breedinginsight.services.ProgramService; +import org.breedinginsight.services.ProgramUserService; +import org.breedinginsight.services.RoleService; import org.breedinginsight.services.exceptions.DoesNotExistException; +import org.breedinginsight.services.exceptions.UnprocessableEntityException; +import org.breedinginsight.utilities.Utilities; import org.breedinginsight.utilities.response.mappers.ExperimentQueryMapper; import javax.inject.Inject; import javax.validation.Valid; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j @Controller @@ -36,12 +48,20 @@ public class ExperimentController { private final BrAPITrialService experimentService; private final ExperimentQueryMapper experimentQueryMapper; private final ProgramService programService; + private final ExperimentalCollaboratorService experimentalCollaboratorService; + private final SecurityService securityService; + private final ProgramUserService programUserService; + private final RoleService roleService; @Inject - public ExperimentController(BrAPITrialService experimentService, ExperimentQueryMapper experimentQueryMapper, ProgramService programService) { + public ExperimentController(BrAPITrialService experimentService, ExperimentQueryMapper experimentQueryMapper, ProgramService programService, ExperimentalCollaboratorService experimentalCollaboratorService, SecurityService securityService, ProgramUserService programUserService, RoleService roleService) { this.experimentService = experimentService; this.experimentQueryMapper = experimentQueryMapper; this.programService = programService; + this.experimentalCollaboratorService = experimentalCollaboratorService; + this.securityService = securityService; + this.programUserService = programUserService; + this.roleService = roleService; } @Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/export{?queryParams*}") @@ -129,7 +149,7 @@ public HttpResponse> createSubEntityDataset( * @param experimentId The UUID of the experiment. * @return An HttpResponse with a Response object containing a list of DatasetMetadata. * @throws DoesNotExistException if the program does not exist. - * @throws ApiException if an error occurs while retrieving the datasets. + * @throws ApiException if an error occurs while retrieving the datasets */ @Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/datasets") @ExperimentCollaboratorSecured @@ -148,4 +168,136 @@ public HttpResponse>> getDatasets( return HttpResponse.ok(response); } + + /** + * Adds a record to the experiment_program_user_role table + * @param programId The UUID of the program + * @param experimentId The UUID of the experiment + * @param request ExperimentalCollaboratorRequest containing the UUID of the bi user to add as a collaborator to the experiemnt + * @return HttpResponse containing the newly created ExperimentProgramUserRoleEntity + */ + @Post("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/collaborators") + @ProgramSecured(roles = {ProgramSecuredRole.PROGRAM_ADMIN, ProgramSecuredRole.SYSTEM_ADMIN}) + @Produces(MediaType.APPLICATION_JSON) + public HttpResponse> createExperimentalCollaborator( + @PathVariable("programId") UUID programId, + @PathVariable("experimentId") UUID experimentId, + @Body @Valid ExperimentCollaboratorRequest request + ) { + try { + //Check if program exists + Optional programOptional = programService.getById(programId); + if (programOptional.isEmpty()) { + return HttpResponse.status(HttpStatus.NOT_FOUND, "Program does not exist"); + } + + //Check if program user exists + Optional programUserOptional = programUserService.getProgramUserbyId(programId, request.getUserId()); + if (programUserOptional.isEmpty()) { + return HttpResponse.status(HttpStatus.NOT_FOUND, "Program user does not exist"); + } + + //get active user creating the collaborator + AuthenticatedUser createdByUser = securityService.getUser(); + UUID programUserId = programUserOptional.get().getId(); + UUID createdByUserId = createdByUser.getId(); + Response response = new Response(experimentalCollaboratorService.createExperimentalCollaborator(programUserId,experimentId,createdByUserId)); + return HttpResponse.ok(response); + } catch (Exception e){ + log.info(e.getMessage()); + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); + } + + } + + /** + * Returns an array of collaborators for given experiment filterable using the active query parameter + * @param programId The UUID of the program + * @param experimentId The UUID of the experiment + * @param active true if querying for collaborators added as a collaborator to the experiment, false if querying for collaborators not added + * @return list of ExperimentalCollaboratorResponse + * Response includes name and email as a convenience to the front end to avoid making another api call + */ + @Get("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/collaborators{?active}") + @ProgramSecured(roles = {ProgramSecuredRole.PROGRAM_ADMIN, ProgramSecuredRole.SYSTEM_ADMIN}) + @Produces(MediaType.APPLICATION_JSON) + public HttpResponse>>> getExperimentalCollaborators( + @PathVariable("programId") UUID programId, + @PathVariable("experimentId") UUID experimentId, + @QueryValue(defaultValue = "true") Boolean active + ) { + try { + //Get experimental collaborators associated with the experiment + List collaborators = experimentalCollaboratorService.getExperimentalCollaborators(experimentId); + List activeCollaboratorIds = collaborators.stream().map((ExperimentProgramUserRoleEntity::getProgramUserRoleId)).collect(Collectors.toList()); + + //Get roleId for experimental collaborator role + Role experimentalCollabRole = roleService.getRoleByDomain(ProgramSecuredRole.EXPERIMENTAL_COLLABORATOR.toString()).get(); + UUID roleId = experimentalCollabRole.getId(); + + //Get all program users with experimental collaborator role + List collabRoleUsers = programUserService.getProgramUsersByRole(programId, roleId); + List collaboratorResponses = new ArrayList<>(); + + for (ProgramUser collabRoleUser : collabRoleUsers) { + UUID collaboratorId = null; + //check if user is an active collaborator for this experiment + Boolean isThisExpCollab = activeCollaboratorIds.contains(collabRoleUser.getId()); + if (isThisExpCollab) { + collaboratorId = collaborators.get(activeCollaboratorIds.indexOf(collabRoleUser.getId())).getId(); + } + + //If active, want to retrieve experimental collaborators added to the experiment + //If not active, want to retrieve experimental collaborators not added to the experiment + if ((active && isThisExpCollab) || (!active && !isThisExpCollab)) { + ExperimentalCollaboratorResponse collabResponse = new ExperimentalCollaboratorResponse(); + collabResponse.setActive(active); + collabResponse.setEmail(collabRoleUser.getUser().getEmail()); + collabResponse.setName(collabRoleUser.getUser().getName()); + collabResponse.setUserId(collabRoleUser.getUserId()); + collabResponse.setCollaboratorId(collaboratorId); + collaboratorResponses.add(collabResponse); + } + } + + List metadataStatus = new ArrayList<>(); + metadataStatus.add(new Status(StatusCode.INFO, "Successful Query")); + //TODO: paging + Pagination pagination = new Pagination(collaborators.size(), collaborators.size(), 1, 0); + Metadata metadata = new Metadata(pagination, metadataStatus); + + Response>> response = new Response(metadata, new DataResponse<>(collaboratorResponses)); + return HttpResponse.ok(response); + } catch (Exception e) { + log.info(e.getMessage(), e); + HttpResponse response = HttpResponse.serverError(); + return response; + } + + } + + /** + * Removes record from experiment_program_user_role table + * @param programId The UUID of the program + * @param experimentId The UUID of the experiment + * @param collaboratorId The UUID of the collaborator, referring to a unique experiment-program user role combo in the experiment_program_user_role table + * @return A Http Response + */ + @Delete("/${micronaut.bi.api.version}/programs/{programId}/experiments/{experimentId}/collaborators/{collaboratorId}") + @ProgramSecured(roles = {ProgramSecuredRole.PROGRAM_ADMIN, ProgramSecuredRole.SYSTEM_ADMIN}) + @Produces(MediaType.APPLICATION_JSON) + public HttpResponse deleteExperimentalCollaborator( + @PathVariable("programId") UUID programId, + @PathVariable("experimentId") UUID experimentId, + @PathVariable("collaboratorId") UUID collaboratorId + ) { + try { + experimentalCollaboratorService.deleteExperimentalCollaborator(collaboratorId); + return HttpResponse.ok(); + } catch (Exception e) { + log.error("Error deleting experimental collaborator.\n\tprogramId: " + programId + "\n\texperimentId: " + experimentId + "\n\tcollaboratorId: " + collaboratorId); + throw e; + } + + } } diff --git a/src/main/java/org/breedinginsight/daos/ExperimentalCollaboratorDAO.java b/src/main/java/org/breedinginsight/daos/ExperimentalCollaboratorDAO.java index 21e7dbaa7..cb0871dc3 100644 --- a/src/main/java/org/breedinginsight/daos/ExperimentalCollaboratorDAO.java +++ b/src/main/java/org/breedinginsight/daos/ExperimentalCollaboratorDAO.java @@ -50,7 +50,7 @@ public ExperimentalCollaboratorDAO(Configuration config, DSLContext dsl) { this.dsl = dsl; } - public ExperimentProgramUserRoleEntity create(UUID experimentId, UUID programUserRoleId, UUID userId) { + public ExperimentProgramUserRoleEntity create(UUID experimentId, UUID programUserRoleId, UUID createdByUserId) { return dsl.insertInto(EXPERIMENT_PROGRAM_USER_ROLE) .columns(EXPERIMENT_PROGRAM_USER_ROLE.EXPERIMENT_ID, EXPERIMENT_PROGRAM_USER_ROLE.PROGRAM_USER_ROLE_ID, @@ -60,9 +60,9 @@ public ExperimentProgramUserRoleEntity create(UUID experimentId, UUID programUse EXPERIMENT_PROGRAM_USER_ROLE.UPDATED_AT) .values(experimentId, programUserRoleId, - userId, + createdByUserId, OffsetDateTime.now(), - userId, + createdByUserId, OffsetDateTime.now()) .returning(EXPERIMENT_PROGRAM_USER_ROLE.fields()) .fetchOneInto(ExperimentProgramUserRoleEntity.class); diff --git a/src/main/java/org/breedinginsight/services/ExperimentalCollaboratorService.java b/src/main/java/org/breedinginsight/services/ExperimentalCollaboratorService.java index f6fe07390..f0349c9e9 100644 --- a/src/main/java/org/breedinginsight/services/ExperimentalCollaboratorService.java +++ b/src/main/java/org/breedinginsight/services/ExperimentalCollaboratorService.java @@ -37,8 +37,8 @@ public ExperimentalCollaboratorService(ExperimentalCollaboratorDAO experimentalC this.experimentalCollaboratorDAO = experimentalCollaboratorDAO; } - public ExperimentProgramUserRoleEntity createExperimentalCollaborator(UUID programUserRoleId, UUID experimentId, UUID userId) { - return this.experimentalCollaboratorDAO.create(experimentId, programUserRoleId, userId); + public ExperimentProgramUserRoleEntity createExperimentalCollaborator(UUID programUserRoleId, UUID experimentId, UUID createdByUserId) { + return this.experimentalCollaboratorDAO.create(experimentId, programUserRoleId, createdByUserId); } public List getExperimentalCollaborators(UUID experimentId) { @@ -47,7 +47,7 @@ public List getExperimentalCollaborators(UUID e } public void deleteExperimentalCollaborator(UUID collaboratorId) { - // Note: collaboratorId is the PK of the experiment_program_user_role table. + // Note: collaboratorId is the primary key of the experiment_program_user_role table. this.experimentalCollaboratorDAO.deleteById(collaboratorId); } } diff --git a/src/main/java/org/breedinginsight/services/RoleService.java b/src/main/java/org/breedinginsight/services/RoleService.java index ecc9fc4e5..63165fdf0 100644 --- a/src/main/java/org/breedinginsight/services/RoleService.java +++ b/src/main/java/org/breedinginsight/services/RoleService.java @@ -17,6 +17,9 @@ package org.breedinginsight.services; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; import lombok.extern.slf4j.Slf4j; import org.breedinginsight.dao.db.tables.daos.RoleDao; import org.breedinginsight.dao.db.tables.pojos.RoleEntity; @@ -66,4 +69,13 @@ public List getRolesByIds(List roleIds) { return roles; } + public Optional getRoleByDomain(String domain) { + RoleEntity role = dao.fetchByDomain(domain).get(0); + if (role == null) { + return Optional.empty(); + } + + return Optional.of(new Role(role)); + } + } diff --git a/src/test/java/org/breedinginsight/brapi/v2/ExperimentControllerIntegrationTest.java b/src/test/java/org/breedinginsight/api/v1/controller/ExperimentControllerIntegrationTest.java similarity index 62% rename from src/test/java/org/breedinginsight/brapi/v2/ExperimentControllerIntegrationTest.java rename to src/test/java/org/breedinginsight/api/v1/controller/ExperimentControllerIntegrationTest.java index d9de6ab3a..e2961bb9f 100644 --- a/src/test/java/org/breedinginsight/brapi/v2/ExperimentControllerIntegrationTest.java +++ b/src/test/java/org/breedinginsight/api/v1/controller/ExperimentControllerIntegrationTest.java @@ -1,4 +1,4 @@ -package org.breedinginsight.brapi.v2; +package org.breedinginsight.api.v1.controller; import com.google.gson.*; import io.kowalski.fannypack.FannyPack; @@ -8,6 +8,7 @@ import io.micronaut.http.MediaType; import io.micronaut.http.client.RxHttpClient; import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.netty.cookies.NettyCookie; import io.micronaut.test.annotation.MicronautTest; import io.reactivex.Flowable; @@ -21,14 +22,17 @@ import org.breedinginsight.api.auth.AuthenticatedUser; import org.breedinginsight.api.model.v1.request.ProgramRequest; import org.breedinginsight.api.model.v1.request.SpeciesRequest; -import org.breedinginsight.api.v1.controller.TestTokenValidator; import org.breedinginsight.brapi.v2.dao.BrAPIGermplasmDAO; import org.breedinginsight.brapi.v2.services.BrAPITrialService; import org.breedinginsight.brapps.importer.ImportTestUtils; import org.breedinginsight.brapps.importer.model.exports.FileType; import org.breedinginsight.brapps.importer.model.imports.experimentObservation.ExperimentObservation; import org.breedinginsight.dao.db.enums.DataType; +import org.breedinginsight.dao.db.tables.daos.RoleDao; +import org.breedinginsight.dao.db.tables.pojos.ProgramUserRoleEntity; +import org.breedinginsight.dao.db.tables.pojos.RoleEntity; import org.breedinginsight.dao.db.tables.pojos.SpeciesEntity; +import org.breedinginsight.daos.ProgramUserDAO; import org.breedinginsight.daos.SpeciesDAO; import org.breedinginsight.daos.UserDAO; import org.breedinginsight.model.*; @@ -43,6 +47,7 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import tech.tablesaw.api.ColumnType; import tech.tablesaw.api.Row; import tech.tablesaw.api.Table; @@ -52,6 +57,7 @@ import java.util.*; import java.util.stream.Collectors; import static io.micronaut.http.HttpRequest.*; +import static org.junit.Assert.assertNotEquals; import static org.junit.jupiter.api.Assertions.*; @MicronautTest @@ -65,6 +71,8 @@ public class ExperimentControllerIntegrationTest extends BrAPITest { private final List> rows = new ArrayList<>(); private final List columns = ExperimentFileColumns.getOrderedColumns(); private List traits; + private User testUser; + private User otherTestUser; @Property(name = "brapi.server.reference-source") private String BRAPI_REFERENCE_SOURCE; @@ -80,6 +88,10 @@ public class ExperimentControllerIntegrationTest extends BrAPITest { private BrAPITrialService experimentService; @Inject private BrAPIGermplasmDAO germplasmDAO; + @Inject + private ProgramUserDAO programUserDAO; + @Inject + private RoleDao roleDao; @Inject @Client("/${micronaut.bi.api.version}") @@ -99,7 +111,9 @@ void setup() throws Exception { FannyPack brapiFp = FannyPack.fill("src/test/resources/sql/brapi/species.sql"); // Test User - User testUser = userDAO.getUserByOrcId(TestTokenValidator.TEST_USER_ORCID).orElseThrow(Exception::new); + testUser = userDAO.getUserByOrcId(TestTokenValidator.TEST_USER_ORCID).orElseThrow(Exception::new); + otherTestUser = userDAO.getUserByOrcId(TestTokenValidator.OTHER_TEST_USER_ORCID).orElseThrow(Exception::new); + dsl.execute(securityFp.get("InsertSystemRoleAdmin"), testUser.getId().toString()); // Species @@ -349,6 +363,334 @@ void downloadSubEntityDataset(String extension) { parseAndCheck(plantBodyStream, extension, false, plantRows, false, 23); } + /** + * Tests for Experimental Collaborator endpoints + */ + + /** + * Create Experimental Collaborator Invalid Id + * GIVEN POST /v1/programs/{programId}/experiments/{experimentId}/collaborators + * WHEN an invalid id is passed in the body of the request + * THEN response should be 404 + */ + @Test + public void postExperimentalCollaboratorsInvalidId() { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("programUserId", "f47ac10b-58cc-4372-a567-0e02b2c3d479"); + + Flowable> call = client.exchange( + POST(String.format("/programs/%s/experiments/%s/collaborators", program.getId().toString(), experimentId), requestBody.toString()) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + HttpClientResponseException e = Assertions.assertThrows(HttpClientResponseException.class, () -> { + HttpResponse response = call.blockingFirst(); + }); + + assertEquals(HttpStatus.NOT_FOUND, e.getStatus()); + } + + /** + * Create and Delete Experimental Collaborator + */ + @Test + public void postAndDeleteExperimentalCollaborator() { + + RoleEntity roleEntity = roleDao.fetchByDomain("Experimental Collaborator").get(0); + + ProgramUserRoleEntity programUserEntity = ProgramUserRoleEntity.builder() + .userId(otherTestUser.getId()) + .programId(program.getId()) + .roleId(roleEntity.getId()) + .createdBy(testUser.getId()) + .updatedBy(testUser.getId()) + .build(); + + programUserDAO.insert(programUserEntity); + ProgramUser programUser = programUserDAO.getProgramUser(program.getId(), otherTestUser.getId()); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", otherTestUser.getId().toString()); + + Flowable> call = client.exchange( + POST(String.format("/programs/%s/experiments/%s/collaborators", program.getId().toString(), experimentId), requestBody.toString()) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + HttpResponse response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + String id = result.get("id").getAsString(); + Assertions.assertNotEquals(null, id); + + // check count = 1 + call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=true", program.getId().toString(), experimentId)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + assertEquals(1, data.size()); + JsonObject collaborator = data.get(0).getAsJsonObject(); + + assertNotEquals(collaborator.get("collaboratorId").getAsString(),null, "Expected not null for collaboratorId"); + assertEquals(collaborator.get("userId").getAsString(), otherTestUser.getId().toString(), "Unexpected userId"); + assertEquals(collaborator.get("name").getAsString(), otherTestUser.getName(), "Unexpected name"); + assertEquals(collaborator.get("email").getAsString(), otherTestUser.getEmail(), "Unexpected email"); + assertEquals(collaborator.get("active").getAsBoolean(), true, "Unexpected active status"); + + // now delete + call = client.exchange( + DELETE(String.format("/programs/%s/experiments/%s/collaborators/%s", program.getId().toString(), experimentId, id)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + + // check count = 0 + call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=true", program.getId().toString(), experimentId)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + data = result.getAsJsonArray("data"); + assertEquals(0, data.size()); + + // clean up + programUserDAO.deleteProgramUserRoles(program.getId(), otherTestUser.getId()); + + } + + /** + * Get Experimental Collaborators None + * GIVEN GET /v1/programs/{programId}/experiments/{experimentId}/collaborators?active=true|false + * WHEN no users have been added as experiment collaborators + * AND no program users with Experimental Collaborator role exist in program + * THEN response should be an empty array regardless of active query parameter value + * + * test-registered-user has Program Administration role in program + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void getExperimentalCollaboratorsNone(boolean active) { + + Flowable> call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=%s", program.getId().toString(), experimentId, active)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + HttpResponse response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + assertEquals(0, data.size()); + } + + /** + * Get Experimental Collaborators No Active + * GIVEN GET /v1/programs/{programId}/experiments/{experimentId}/collaborators?active=true|false + * WHEN no users have been added as experiment collaborators + * AND one or more program users with Experimental Collaborator role exist in program + * THEN response should be: + * empty array when active=true + * array with program user when active=false + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void getExperimentalCollaboratorsNoActive(boolean active) { + + // add a program user with the experimental collaborator role + FannyPack securityFp = FannyPack.fill("src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql"); + dsl.execute(securityFp.get("InsertProgramRolesExperimentalCollaborator"), otherTestUser.getId().toString(), program.getId()); + + ProgramUser programUser = programUserDAO.getProgramUser(program.getId(), otherTestUser.getId()); + + Flowable> call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=%s", program.getId().toString(), experimentId, active)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + HttpResponse response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + + if (active) { + assertEquals(0, data.size()); + } else { + assertEquals(1, data.size()); + JsonObject collaborator = data.get(0).getAsJsonObject(); + assertNull(collaborator.get("collaboratorId"), "Expected null for id"); + assertEquals(collaborator.get("userId").getAsString(), otherTestUser.getId().toString(), "Unexpected userId"); + assertEquals(collaborator.get("name").getAsString(), otherTestUser.getName(), "Unexpected name"); + assertEquals(collaborator.get("email").getAsString(), otherTestUser.getEmail(), "Unexpected email"); + assertEquals(collaborator.get("active").getAsBoolean(), false, "Unexpected active status"); + } + + // delete program user from setup + // NOTE: if test fails this may not be run and could impact other tests results + dsl.execute(securityFp.get("DeleteProgramUser"), otherTestUser.getId().toString()); + + } + + /** + * Get Experimental Collaborators Active + * GIVEN GET /v1/programs/{programId}/experiments/{experimentId}/collaborators?active=true|false + * WHEN program user has been added to experiment as collaborator + * AND program user with Experimental Collaborator role exists in program + * THEN response should be: + * array with program user when active=true + * empty array when active=false + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void getExperimentalCollaboratorsActive(boolean active) { + // add a program user with the experimental collaborator role + FannyPack securityFp = FannyPack.fill("src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql"); + dsl.execute(securityFp.get("InsertProgramRolesExperimentalCollaborator"), otherTestUser.getId().toString(), program.getId()); + + // add user as experimental collaborator + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", otherTestUser.getId().toString()); + + Flowable> call = client.exchange( + POST(String.format("/programs/%s/experiments/%s/collaborators", program.getId().toString(), experimentId), requestBody.toString()) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + HttpResponse response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + String id = result.get("id").getAsString(); + Assertions.assertNotEquals(null, id); + + // get experimental collaborators + call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=%s", program.getId().toString(), experimentId, active)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + + if (active) { + assertEquals(1, data.size()); + } else { + assertEquals(0, data.size()); + } + + // cleanup - delete collaborator + call = client.exchange( + DELETE(String.format("/programs/%s/experiments/%s/collaborators/%s", program.getId().toString(), experimentId, id)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + + // delete program user from setup + // NOTE: if test fails this may not be run and could impact other tests results + dsl.execute(securityFp.get("DeleteProgramUser"), otherTestUser.getId().toString()); + } + + /** + * Get Experimental Collaborators Deactivated from Program + * GIVEN GET /v1/programs/{programId}/experiments/{experimentId}/collaborators?active=true|false + * WHEN program user has been added to experiment as collaborator + * AND after being added as a collaborator, program user is deactivated from program + * THEN response should be empty array regardless of active query parameter value (assumes single user) + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void getExperimentalCollaboratorsDeactivated(boolean active) { + // add a program user with the experimental collaborator role + FannyPack securityFp = FannyPack.fill("src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql"); + dsl.execute(securityFp.get("InsertProgramRolesExperimentalCollaborator"), otherTestUser.getId().toString(), program.getId()); + + // add user as experimental collaborator + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", otherTestUser.getId().toString()); + + Flowable> call = client.exchange( + POST(String.format("/programs/%s/experiments/%s/collaborators", program.getId().toString(), experimentId), requestBody.toString()) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + HttpResponse response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + String id = result.get("id").getAsString(); + Assertions.assertNotEquals(null, id); + + // deactivate program user + call = client.exchange( + DELETE(String.format("/programs/%s/users/%s", program.getId().toString(), otherTestUser.getId().toString())) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + + // get experimental collaborators + call = client.exchange( + GET(String.format("/programs/%s/experiments/%s/collaborators?active=%s", program.getId().toString(), experimentId, active)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + response = call.blockingFirst(); + + assertEquals(HttpStatus.OK, response.getStatus()); + + result = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("result"); + JsonArray data = result.getAsJsonArray("data"); + + // should be empty regardless of active query parameter value + assertEquals(0, data.size()); + + // cleanup - remove collaborator + call = client.exchange( + DELETE(String.format("/programs/%s/experiments/%s/collaborators/%s", program.getId().toString(), experimentId, id)) + .contentType(MediaType.APPLICATION_JSON) + .cookie(new NettyCookie("phylo-token", "test-registered-user")), String.class + ); + + response = call.blockingFirst(); + assertEquals(HttpStatus.OK, response.getStatus()); + + // delete program user from setup + // NOTE: if test fails this may not be run and could impact other tests results + dsl.execute(securityFp.get("DeleteProgramUser"), otherTestUser.getId().toString()); + } + + private List> buildSubEntityRows(List> topLevelRows, String entityName, int repeatedMeasures) { List> plantRows = new ArrayList<>(); for (Map row : topLevelRows) { diff --git a/src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql b/src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql index 17f08c19d..7bd85d9be 100644 --- a/src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql +++ b/src/test/resources/sql/ProgramSecuredAnnotationRuleIntegrationTest.sql @@ -53,6 +53,34 @@ bi_user join role on role.domain = 'Program Administrator' where bi_user.name = 'system'; +-- name: InsertProgramRolesExperimentalCollaborator + +insert into program_user_role (user_id, program_id, role_id, created_by, updated_by) +select + ?::uuid, ?::uuid, role.id, bi_user.id, bi_user.id +from + bi_user + join role on role.domain = 'Experimental Collaborator' +where bi_user.name = 'system'; + +-- name: DeleteExperimentalCollaboratorProgramUsers + +DELETE FROM program_user_role + USING role +WHERE program_user_role.role_id = role.id + AND role.domain = 'Experimental Collaborator'; + +-- name: DeleteExperimentalCollaborator + +DELETE FROM experiment_program_user_role +WHERE id = :id:uuid; + + +-- name: DeleteProgramUser + +DELETE FROM program_user_role +WHERE user_id = :id::uuid; + -- name: InsertSystemRoleAdmin insert into system_user_role (bi_user_id, system_role_id, created_by, updated_by)