From 184e78fd1855093bb84692a39c2bdfe5d77262a8 Mon Sep 17 00:00:00 2001 From: Azizbek Khushvakov Date: Tue, 13 Aug 2024 19:04:58 +0500 Subject: [PATCH] [MODCONSKC-19] - Implement sharing of authorization roles --- descriptors/ModuleDescriptor-template.json | 50 ++- pom.xml | 28 ++ .../controller/SharingRoleController.java | 39 +++ .../domain/entity/SharingRoleEntity.java | 41 +++ .../repository/SharingRoleRepository.java | 21 ++ .../service/impl/SharingPolicyService.java | 4 +- .../service/impl/SharingRoleService.java | 119 +++++++ .../service/impl/SharingSettingService.java | 4 +- .../swagger.api/schemas/sharingRole.yaml | 44 +++ .../swagger.api/sharing_policies.yaml | 2 +- .../resources/swagger.api/sharing_roles.yaml | 125 ++++++++ .../controller/SharingRoleControllerTest.java | 63 ++++ .../service/SharingPolicyServiceTest.java | 6 +- .../service/SharingRoleServiceTest.java | 290 ++++++++++++++++++ .../folio/consortia/support/EntityUtils.java | 39 ++- .../sharing_roles/sharing_role_request.json | 9 + .../sharing_role_request_without_payload.json | 4 + 17 files changed, 877 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/folio/consortia/controller/SharingRoleController.java create mode 100644 src/main/java/org/folio/consortia/domain/entity/SharingRoleEntity.java create mode 100644 src/main/java/org/folio/consortia/repository/SharingRoleRepository.java create mode 100644 src/main/java/org/folio/consortia/service/impl/SharingRoleService.java create mode 100644 src/main/resources/swagger.api/schemas/sharingRole.yaml create mode 100644 src/main/resources/swagger.api/sharing_roles.yaml create mode 100644 src/test/java/org/folio/consortia/controller/SharingRoleControllerTest.java create mode 100644 src/test/java/org/folio/consortia/service/SharingRoleServiceTest.java create mode 100644 src/test/resources/mockdata/sharing_roles/sharing_role_request.json create mode 100644 src/test/resources/mockdata/sharing_roles/sharing_role_request_without_payload.json diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index f8222c60..d921baa9 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -21,6 +21,14 @@ { "id": "inventory", "version": "13.0" + }, + { + "id": "roles", + "version": "1.1" + }, + { + "id": "policies", + "version": "1.1" } ], "optional": [ @@ -360,6 +368,26 @@ "consortia.sharing-policies.item.delete" ], "modulePermissions": [] + }, + { + "methods": [ + "POST" + ], + "pathPattern": "/consortia/{consortiumId}/sharing/roles", + "permissionsRequired": [ + "consortia.sharing-roles.item.post" + ], + "modulePermissions": [] + }, + { + "methods": [ + "DELETE" + ], + "pathPattern": "/consortia/{consortiumId}/sharing/roles/{rolesId}", + "permissionsRequired": [ + "consortia.sharing-roles.item.delete" + ], + "modulePermissions": [] } ] }, @@ -433,7 +461,9 @@ "consortia.sharing-settings.item.post", "consortia.sharing-settings.item.delete", "consortia.sharing-policies.item.post", - "consortia.sharing-policies.item.delete" + "consortia.sharing-policies.item.delete", + "consortia.sharing-roles.item.post", + "consortia.sharing-roles.item.delete" ] }, { @@ -572,6 +602,16 @@ "permissionName": "consortia.sharing-policies.item.delete", "displayName": "delete sharing policy", "description": "Delete sharing policy" + }, + { + "permissionName": "consortia.sharing-roles.item.post", + "displayName": "post sharing role", + "description": "Create sharing roles" + }, + { + "permissionName": "consortia.sharing-roles.item.delete", + "displayName": "delete sharing role", + "description": "Delete sharing role" } ], "metadata": { @@ -667,7 +707,13 @@ "user-settings.custom-fields.item.post", "user-settings.custom-fields.collection.get", "ui-users.editperms", - "capabilities.collection.get" + "capabilities.collection.get", + "roles.item.post", + "roles.item.put", + "roles.item.delete", + "policies.item.post", + "policies.item.put", + "policies.item.delete" ] } }, diff --git a/pom.xml b/pom.xml index 755498c2..06fded63 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,8 @@ ${project.basedir}/src/main/resources/swagger.api/sharing_instances.yaml ${project.basedir}/src/main/resources/swagger.api/sharing_settings.yaml ${project.basedir}/src/main/resources/swagger.api/sharing_policies.yaml + ${project.basedir}/src/main/resources/swagger.api/sharing_roles.yaml + 8.1.2 @@ -559,6 +561,32 @@ + + sharing roles + + generate + + + ${sharing_roles.yaml.file} + ${project.build.directory}/generated-sources + spring + ${project.groupId}.consortia.domain.dto + ${project.groupId}.consortia.rest.resource + true + true + true + true + false + true + ApiUtil.java + true + + java + true + true + + + publish_coordinator diff --git a/src/main/java/org/folio/consortia/controller/SharingRoleController.java b/src/main/java/org/folio/consortia/controller/SharingRoleController.java new file mode 100644 index 00000000..a2afeb0b --- /dev/null +++ b/src/main/java/org/folio/consortia/controller/SharingRoleController.java @@ -0,0 +1,39 @@ +package org.folio.consortia.controller; + +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +import java.util.UUID; + +import lombok.RequiredArgsConstructor; +import org.folio.consortia.domain.dto.SharingRoleDeleteResponse; +import org.folio.consortia.domain.dto.SharingRoleRequest; +import org.folio.consortia.domain.dto.SharingRoleResponse; +import org.folio.consortia.rest.resource.RolesApi; +import org.folio.consortia.service.impl.SharingRoleService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/consortia/{consortiumId}/sharing") +@RequiredArgsConstructor +public class SharingRoleController implements RolesApi { + + private final SharingRoleService sharingRoleService; + + @Override + public ResponseEntity startSharingRole(UUID consortiumId, SharingRoleRequest sharingRoleRequest) { + return ResponseEntity + .status(CREATED) + .body(sharingRoleService.start(consortiumId, sharingRoleRequest)); + } + + @Override + public ResponseEntity deleteSharingRole(UUID consortiumId, UUID roleId, + SharingRoleRequest sharingRoleRequest) { + return ResponseEntity + .status(OK) + .body(sharingRoleService.delete(consortiumId, roleId, sharingRoleRequest)); + } +} diff --git a/src/main/java/org/folio/consortia/domain/entity/SharingRoleEntity.java b/src/main/java/org/folio/consortia/domain/entity/SharingRoleEntity.java new file mode 100644 index 00000000..d70e4f31 --- /dev/null +++ b/src/main/java/org/folio/consortia/domain/entity/SharingRoleEntity.java @@ -0,0 +1,41 @@ +package org.folio.consortia.domain.entity; + +import java.util.Objects; +import java.util.UUID; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.folio.consortia.domain.entity.base.AuditableEntity; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +@Table(name = "sharing_role") +public class SharingRoleEntity extends AuditableEntity { + @Id + private UUID id; + private UUID roleId; + private String tenantId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SharingRoleEntity that = (SharingRoleEntity) o; + return Objects.equals(id, that.id) + && Objects.equals(roleId, that.roleId) + && Objects.equals(tenantId, that.tenantId); + } + + @Override + public int hashCode() { + return Objects.hash(id, roleId, tenantId); + } +} diff --git a/src/main/java/org/folio/consortia/repository/SharingRoleRepository.java b/src/main/java/org/folio/consortia/repository/SharingRoleRepository.java new file mode 100644 index 00000000..c53c29ec --- /dev/null +++ b/src/main/java/org/folio/consortia/repository/SharingRoleRepository.java @@ -0,0 +1,21 @@ +package org.folio.consortia.repository; + +import java.util.Set; +import java.util.UUID; + +import org.folio.consortia.domain.entity.SharingRoleEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface SharingRoleRepository extends JpaRepository { + + @Query("SELECT sr.tenantId FROM SharingRoleEntity sr WHERE sr.roleId = ?1") + Set findTenantsByRoleId(UUID roleId); + + boolean existsByRoleId(UUID roleId); + + @Modifying + @Query("DELETE FROM SharingRoleEntity sr WHERE sr.roleId = ?1") + void deleteByRoleId(UUID roleId); +} diff --git a/src/main/java/org/folio/consortia/service/impl/SharingPolicyService.java b/src/main/java/org/folio/consortia/service/impl/SharingPolicyService.java index a963efa8..964da3bb 100644 --- a/src/main/java/org/folio/consortia/service/impl/SharingPolicyService.java +++ b/src/main/java/org/folio/consortia/service/impl/SharingPolicyService.java @@ -82,10 +82,10 @@ protected PublicationRequest createPublicationRequest(SharingPolicyRequest shari publicationRequest.setMethod(httpMethod); String url = sharingPolicyRequest.getUrl(); if (httpMethod.equals(HttpMethod.PUT.toString()) || httpMethod.equals(HttpMethod.DELETE.toString())) { - url += "/" + sharingPolicyRequest.getPolicyId(); + url += "/" + getConfigId(sharingPolicyRequest); } publicationRequest.setUrl(url); - publicationRequest.setPayload(sharingPolicyRequest.getPayload()); + publicationRequest.setPayload(getPayload(sharingPolicyRequest)); publicationRequest.setTenants(new HashSet<>()); return publicationRequest; } diff --git a/src/main/java/org/folio/consortia/service/impl/SharingRoleService.java b/src/main/java/org/folio/consortia/service/impl/SharingRoleService.java new file mode 100644 index 00000000..287b97bb --- /dev/null +++ b/src/main/java/org/folio/consortia/service/impl/SharingRoleService.java @@ -0,0 +1,119 @@ +package org.folio.consortia.service.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.ObjectUtils; +import org.folio.consortia.domain.dto.PublicationRequest; +import org.folio.consortia.domain.dto.SharingRoleDeleteResponse; +import org.folio.consortia.domain.dto.SharingRoleRequest; +import org.folio.consortia.domain.dto.SharingRoleResponse; +import org.folio.consortia.domain.entity.SharingRoleEntity; +import org.folio.consortia.exception.ResourceNotFoundException; +import org.folio.consortia.repository.SharingRoleRepository; +import org.folio.consortia.service.BaseSharingService; +import org.folio.consortia.service.ConsortiumService; +import org.folio.consortia.service.PublicationService; +import org.folio.consortia.service.TenantService; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.service.SystemUserScopedExecutionService; +import org.springframework.core.task.TaskExecutor; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; + +@Service +@Log4j2 +public class SharingRoleService extends BaseSharingService { + + private final SharingRoleRepository sharingRoleRepository; + + public SharingRoleService(TenantService tenantService, + ConsortiumService consortiumService, + SystemUserScopedExecutionService systemUserScopedExecutionService, + PublicationService publicationService, FolioExecutionContext folioExecutionContext, + ObjectMapper objectMapper, TaskExecutor asyncTaskExecutor, + SharingRoleRepository sharingRoleRepository) { + super(tenantService, consortiumService, systemUserScopedExecutionService, publicationService, folioExecutionContext, objectMapper, asyncTaskExecutor); + this.sharingRoleRepository = sharingRoleRepository; + } + + @Override + protected UUID getConfigId(SharingRoleRequest sharingRoleRequest) { + return sharingRoleRequest.getRoleId(); + } + + @Override + protected Object getPayload(SharingRoleRequest sharingRoleRequest) { + return sharingRoleRequest.getPayload(); + } + + @Override + protected void validateSharingConfigRequestOrThrow(UUID roleId, SharingRoleRequest sharingRoleRequest) { + if (ObjectUtils.notEqual(getConfigId(sharingRoleRequest), roleId)) { + throw new IllegalArgumentException("Mismatch id in path to roleId in request body"); + } + if (Objects.isNull(getPayload(sharingRoleRequest))) { + throw new IllegalArgumentException("Payload must not be null"); + } + if (!sharingRoleRepository.existsByRoleId(roleId)) { + throw new ResourceNotFoundException("roleId", String.valueOf(roleId)); + } + } + + @Override + protected Set findTenantsByConfigId(UUID roleId) { + return sharingRoleRepository.findTenantsByRoleId(roleId); + } + + @Override + protected void saveSharingConfig(List sharingRoleEntityList) { + sharingRoleRepository.saveAll(sharingRoleEntityList); + } + + @Override + protected void deleteSharingConfig(UUID roleId) { + sharingRoleRepository.deleteByRoleId(roleId); + } + + @Override + protected PublicationRequest createPublicationRequest(SharingRoleRequest sharingRoleRequest, String httpMethod) { + PublicationRequest publicationRequest = new PublicationRequest(); + publicationRequest.setMethod(httpMethod); + String url = sharingRoleRequest.getUrl(); + if (httpMethod.equals(HttpMethod.PUT.toString()) || httpMethod.equals(HttpMethod.DELETE.toString())) { + url += "/" + getConfigId(sharingRoleRequest); + } + publicationRequest.setUrl(url); + publicationRequest.setPayload(getPayload(sharingRoleRequest)); + publicationRequest.setTenants(new HashSet<>()); + return publicationRequest; + } + + @Override + protected SharingRoleEntity createSharingConfigEntityFromRequest(SharingRoleRequest sharingRoleRequest, String tenantId) { + SharingRoleEntity sharingRoleEntity = new SharingRoleEntity(); + sharingRoleEntity.setId(UUID.randomUUID()); + sharingRoleEntity.setRoleId(sharingRoleEntity.getRoleId()); + sharingRoleEntity.setTenantId(tenantId); + return sharingRoleEntity; + } + + @Override + protected SharingRoleResponse createSharingConfigResponse(UUID createRolesPcId, UUID updateRolesPcId) { + return new SharingRoleResponse() + .createRolesPCId(createRolesPcId) + .updateRolesPCId(updateRolesPcId); + } + + @Override + protected SharingRoleDeleteResponse createSharingConfigResponse(UUID publishRequestId) { + return new SharingRoleDeleteResponse() + .pcId(publishRequestId); + } +} diff --git a/src/main/java/org/folio/consortia/service/impl/SharingSettingService.java b/src/main/java/org/folio/consortia/service/impl/SharingSettingService.java index 6d31f4d5..1a4682b0 100644 --- a/src/main/java/org/folio/consortia/service/impl/SharingSettingService.java +++ b/src/main/java/org/folio/consortia/service/impl/SharingSettingService.java @@ -89,10 +89,10 @@ protected PublicationRequest createPublicationRequest(SharingSettingRequest shar publicationRequest.setMethod(httpMethod); String url = sharingSettingRequest.getUrl(); if (httpMethod.equals(HttpMethod.PUT.toString()) || httpMethod.equals(HttpMethod.DELETE.toString())) { - url += "/" + sharingSettingRequest.getSettingId(); + url += "/" + getConfigId(sharingSettingRequest); } publicationRequest.setUrl(url); - publicationRequest.setPayload(sharingSettingRequest.getPayload()); + publicationRequest.setPayload(getPayload(sharingSettingRequest)); publicationRequest.setTenants(new HashSet<>()); return publicationRequest; } diff --git a/src/main/resources/swagger.api/schemas/sharingRole.yaml b/src/main/resources/swagger.api/schemas/sharingRole.yaml new file mode 100644 index 00000000..2240c34d --- /dev/null +++ b/src/main/resources/swagger.api/schemas/sharingRole.yaml @@ -0,0 +1,44 @@ +SharingRoleRequest: + description: "A JSON schema for the Sharing roles object" + type: object + properties: + roleId: + description: id of sharing role record + type: string + format: uuid + url: + description: URL for publishing requests for consortia tenants + type: string + payload: + description: Http request body + type: object + additionalProperties: false + required: + - roleId + - url + +SharingRoleResponse: + description: "A JSON schema for the Sharing roles object response for post request" + type: object + properties: + createRolesPCId: + type: string + format: uuid + updateRolesPCId: + type: string + format: uuid + additionalProperties: false + required: + - createRolesPCId + - updateRolesPCId + +SharingRoleDeleteResponse: + description: "A JSON schema for the Sharing roles object response for delete request" + type: object + properties: + pcId: + type: string + format: uuid + additionalProperties: false + required: + - pcId diff --git a/src/main/resources/swagger.api/sharing_policies.yaml b/src/main/resources/swagger.api/sharing_policies.yaml index 76d77a86..1ae1ea47 100644 --- a/src/main/resources/swagger.api/sharing_policies.yaml +++ b/src/main/resources/swagger.api/sharing_policies.yaml @@ -8,7 +8,7 @@ servers: paths: /policies: post: - summary: start policy sharing + summary: start sharing policy operationId: startSharingPolicy parameters: - $ref: "#/components/parameters/consortiumId" diff --git a/src/main/resources/swagger.api/sharing_roles.yaml b/src/main/resources/swagger.api/sharing_roles.yaml new file mode 100644 index 00000000..63514a35 --- /dev/null +++ b/src/main/resources/swagger.api/sharing_roles.yaml @@ -0,0 +1,125 @@ +openapi: 3.0.0 +info: + title: Sharing role integration API + description: "Sharing role integration API" + version: 0.0.1 +servers: + - url: /consortia/{consortiumId}/sharing +paths: + /roles: + post: + summary: start sharing role + operationId: startSharingRole + parameters: + - $ref: "#/components/parameters/consortiumId" + requestBody: + $ref: "#/components/requestBodies/SharingRoleBody" + responses: + "201": + $ref: "#/components/responses/SharingRoleResponse" + "400": + $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NotFound" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/Conflict" + "500": + $ref: "#/components/responses/InternalServerError" + /roles/{roleId}: + delete: + summary: delete sharing role + operationId: deleteSharingRole + parameters: + - $ref: "#/components/parameters/consortiumId" + - $ref: "#/components/parameters/roleId" + requestBody: + $ref: "#/components/requestBodies/SharingRoleBody" + responses: + "200": + $ref: "#/components/responses/SharingRoleDeleteResponse" + "400": + $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NotFound" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/Conflict" + "500": + $ref: "#/components/responses/InternalServerError" +components: + requestBodies: + SharingRoleBody: + description: Sharing roles object + required: true + content: + application/json: + schema: + $ref: "schemas/sharingRole.yaml#/SharingRoleRequest" + responses: + SharingRoleResponse: + description: Returns a sharing role object response for post operation + content: + application/json: + schema: + $ref: "schemas/sharingRole.yaml#/SharingRoleResponse" + SharingRoleDeleteResponse: + description: Returns a sharing role response for delete operation + content: + application/json: + schema: + $ref: "schemas/sharingRole.yaml#/SharingRoleDeleteResponse" + NoContent: + description: No content + Conflict: + description: Validation errors + content: + application/json: + schema: + $ref: "schemas/common.yaml#/Errors" + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: "schemas/common.yaml#/Errors" + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: "schemas/common.yaml#/Errors" + Unauthorized: + description: Not authorized to perform requested action + content: + text/plain: + example: unable to perform action -- unauthorized + UnprocessableEntity: + description: Validation errors + content: + application/json: + schema: + $ref: 'schemas/common.yaml#/Errors' + InternalServerError: + description: Internal server error + content: + application/json: + schema: + $ref: "schemas/common.yaml#/Error" + parameters: + consortiumId: + in: path + name: consortiumId + schema: + $ref: "schemas/common.yaml#/uuid" + required: true + description: The ID of consortium + roleId: + in: path + name: roleId + schema: + $ref: "schemas/common.yaml#/uuid" + required: true + description: The ID of role diff --git a/src/test/java/org/folio/consortia/controller/SharingRoleControllerTest.java b/src/test/java/org/folio/consortia/controller/SharingRoleControllerTest.java new file mode 100644 index 00000000..94b74a9b --- /dev/null +++ b/src/test/java/org/folio/consortia/controller/SharingRoleControllerTest.java @@ -0,0 +1,63 @@ +package org.folio.consortia.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + +import org.folio.consortia.base.BaseIT; +import org.folio.consortia.domain.dto.SharingRoleDeleteResponse; +import org.folio.consortia.domain.dto.SharingRoleResponse; +import org.folio.consortia.service.impl.SharingRoleService; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; + +class SharingRoleControllerTest extends BaseIT { + @MockBean + SharingRoleService sharingRoleService; + + @ParameterizedTest + @ValueSource(strings = {"{\"roleId\":\"2844767a-8367-4926-9999-514c35840399\",\"url\":\"/role\",\"payload\":{\"name\":\"ROLE-NAME\",\"source\":\"local\"}}" }) + void shouldStartSharingRole(String body) throws Exception { + var headers = defaultHeaders(); + UUID createRolesPcId = UUID.randomUUID(); + UUID updateRolesPcId = UUID.randomUUID(); + SharingRoleResponse sharingRoleResponse = new SharingRoleResponse() + .createRolesPCId(createRolesPcId) + .updateRolesPCId(updateRolesPcId); + + when(sharingRoleService.start(any(), any())).thenReturn(sharingRoleResponse); + + this.mockMvc.perform( + post("/consortia/7698e46-c3e3-11ed-afa1-0242ac120002/sharing/roles") + .headers(headers) + .content(body) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.createRolesPCId").value(String.valueOf(createRolesPcId))) + .andExpect(jsonPath("$.updateRolesPCId").value(String.valueOf(updateRolesPcId))); + } + + @ParameterizedTest + @ValueSource(strings = {"{\"roleId\":\"2844767a-8367-4926-9999-514c35840399\",\"url\":\"/role\"}" }) + void shouldDeleteSharingRole(String body) throws Exception { + var headers = defaultHeaders(); + UUID pcId = UUID.randomUUID(); + SharingRoleDeleteResponse sharingRoleDeleteResponse = new SharingRoleDeleteResponse().pcId(pcId); + + when(sharingRoleService.delete(any(), any(), any())).thenReturn(sharingRoleDeleteResponse); + + this.mockMvc.perform( + delete("/consortia/7698e46-c3e3-11ed-afa1-0242ac120002/sharing/roles/2844767a-8367-4926-9999-514c35840399") + .headers(headers) + .content(body) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } +} diff --git a/src/test/java/org/folio/consortia/service/SharingPolicyServiceTest.java b/src/test/java/org/folio/consortia/service/SharingPolicyServiceTest.java index 0221c8fd..e25b50a6 100644 --- a/src/test/java/org/folio/consortia/service/SharingPolicyServiceTest.java +++ b/src/test/java/org/folio/consortia/service/SharingPolicyServiceTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.folio.consortia.support.EntityUtils.createJsonNodeForGroupPayload; -import static org.folio.consortia.support.EntityUtils.createJsonNodeForRolePayload; +import static org.folio.consortia.support.EntityUtils.createJsonNodeForPolicyPayload; import static org.folio.consortia.support.EntityUtils.createPublicationDetails; import static org.folio.consortia.support.EntityUtils.createPublicationRequest; import static org.folio.consortia.support.EntityUtils.createPublicationResultCollection; @@ -126,7 +126,7 @@ void shouldStartSharingPolicy() throws JsonProcessingException { when(sharingPolicyRepository.save(any())).thenReturn(new SharingPolicyEntity()); when(folioExecutionContext.getTenantId()).thenReturn("mobius"); when(systemUserScopedExecutionService.executeSystemUserScoped(eq("mobius"), any())).then(SharingPolicyServiceTest::callSecondArgument); - when(objectMapper.convertValue(payload, JsonNode.class)).thenReturn(createJsonNodeForRolePayload()); + when(objectMapper.convertValue(payload, JsonNode.class)).thenReturn(createJsonNodeForPolicyPayload()); var expectedResponse = createSharingPolicyResponse(createPoliciesPcId, updatePoliciesPcId); var actualResponse = sharingPolicyService.start(CONSORTIUM_ID, sharingPolicyRequest); @@ -220,7 +220,7 @@ void shouldUpdateFailedTenantPolicies() throws NoSuchMethodException, Invocation void shouldThrowErrorForNotEqualPolicyIdWithPayloadId() throws JsonProcessingException { var sharingPolicyRequest = getMockDataObject(SHARING_POLICY_REQUEST_SAMPLE_FOR_ROLES, SharingPolicyRequest.class); sharingPolicyRequest.setPolicyId(UUID.randomUUID()); - JsonNode node = createJsonNodeForRolePayload(); + JsonNode node = createJsonNodeForPolicyPayload(); when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); when(objectMapper.convertValue(any(), eq(JsonNode.class))).thenReturn(node); diff --git a/src/test/java/org/folio/consortia/service/SharingRoleServiceTest.java b/src/test/java/org/folio/consortia/service/SharingRoleServiceTest.java new file mode 100644 index 00000000..a0f0463b --- /dev/null +++ b/src/test/java/org/folio/consortia/service/SharingRoleServiceTest.java @@ -0,0 +1,290 @@ +package org.folio.consortia.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.folio.consortia.support.EntityUtils.createJsonNodeForGroupPayload; +import static org.folio.consortia.support.EntityUtils.createJsonNodeForRolePayload; +import static org.folio.consortia.support.EntityUtils.createPublicationDetails; +import static org.folio.consortia.support.EntityUtils.createPublicationRequest; +import static org.folio.consortia.support.EntityUtils.createPublicationResultCollection; +import static org.folio.consortia.support.EntityUtils.createSharingRoleResponse; +import static org.folio.consortia.support.EntityUtils.createSharingRoleResponseForDelete; +import static org.folio.consortia.support.EntityUtils.createTenant; +import static org.folio.consortia.support.EntityUtils.createTenantCollection; +import static org.folio.consortia.support.TestConstants.CONSORTIUM_ID; +import static org.folio.consortia.utils.InputOutputTestUtils.getMockDataObject; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.folio.consortia.domain.dto.PublicationRequest; +import org.folio.consortia.domain.dto.PublicationResponse; +import org.folio.consortia.domain.dto.PublicationStatus; +import org.folio.consortia.domain.dto.SharingRoleRequest; +import org.folio.consortia.domain.dto.Tenant; +import org.folio.consortia.domain.dto.TenantCollection; +import org.folio.consortia.domain.entity.SharingRoleEntity; +import org.folio.consortia.exception.ResourceNotFoundException; +import org.folio.consortia.repository.ConsortiumRepository; +import org.folio.consortia.repository.PublicationStatusRepository; +import org.folio.consortia.repository.SharingRoleRepository; +import org.folio.consortia.service.impl.SharingRoleService; +import org.folio.spring.DefaultFolioExecutionContext; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.FolioModuleMetadata; +import org.folio.spring.integration.XOkapiHeaders; +import org.folio.spring.scope.FolioExecutionContextSetter; +import org.folio.spring.service.SystemUserScopedExecutionService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.task.TaskExecutor; +import org.springframework.http.HttpMethod; +import org.springframework.test.util.ReflectionTestUtils; + +@SpringBootTest +class SharingRoleServiceTest { + private static final String SHARING_ROLE_REQUEST_SAMPLE = "mockdata/sharing_roles/sharing_role_request.json"; + private static final String SHARING_ROLE_REQUEST_SAMPLE_WITHOUT_PAYLOAD = "mockdata/sharing_roles/sharing_role_request_without_payload.json"; + + @InjectMocks + private SharingRoleService sharingRoleService; + @Mock + private ConsortiumRepository consortiumRepository; + @Mock + private ConsortiumService consortiumService; + @Mock + private TaskExecutor asyncTaskExecutor; + @Mock + private TenantService tenantService; + @Mock + private PublicationService publicationService; + @Mock + private PublicationStatusRepository publicationStatusRepository; + @Mock + private SharingRoleRepository sharingRoleRepository; + @Mock + private FolioExecutionContext folioExecutionContext; + @Mock + private SystemUserScopedExecutionService systemUserScopedExecutionService; + @Mock + private ObjectMapper objectMapper; + + @Test + void shouldStartSharingRole() throws JsonProcessingException { + UUID createRolesPcId = UUID.randomUUID(); + UUID updateRolesPcId = UUID.randomUUID(); + Tenant tenant1 = createTenant("tenant1", "tenant1"); + Tenant tenant2 = createTenant("tenant2", "tenant2"); + Set tenantAssociationsWithRole = Set.of("tenant1"); + TenantCollection tenantCollection = createTenantCollection(List.of(tenant1, tenant2)); + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + Map payload = new LinkedHashMap<>(); + payload.put("id", "3844767a-8367-4926-9999-514c35840399"); + payload.put("name", "Role for policy: 104d7a66-c51d-402a-9c9f-3bdcdbbcdbe7"); + payload.put("source", "local"); + + // "tenant1" exists in tenant role association so that tenant1 is in PUT request publication, + // "tenant2" is in POST method publication + var publicationRequestPut = createPublicationRequest(sharingRoleRequest, HttpMethod.PUT.toString()); + publicationRequestPut.setMethod("PUT"); + publicationRequestPut.setTenants(Set.of("tenant1")); + publicationRequestPut.setUrl("/role/3844767a-8367-4926-9999-514c35840399"); + var publicationRequestPost = createPublicationRequest(sharingRoleRequest, HttpMethod.POST.toString()); + publicationRequestPost.setMethod("POST"); + publicationRequestPost.setTenants(Set.of("tenant2")); + + var publicationResponsePost = new PublicationResponse().id(createRolesPcId); + var publicationResponsePut = new PublicationResponse().id(updateRolesPcId); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + when(publicationService.publishRequest(CONSORTIUM_ID, publicationRequestPost)).thenReturn(publicationResponsePost); + when(publicationService.publishRequest(CONSORTIUM_ID, publicationRequestPut)).thenReturn(publicationResponsePut); + when(tenantService.getAll(CONSORTIUM_ID)).thenReturn(tenantCollection); + when(sharingRoleRepository.findTenantsByRoleId(sharingRoleRequest.getRoleId())).thenReturn(tenantAssociationsWithRole); + when(sharingRoleRepository.save(any())).thenReturn(new SharingRoleEntity()); + when(folioExecutionContext.getTenantId()).thenReturn("mobius"); + when(systemUserScopedExecutionService.executeSystemUserScoped(eq("mobius"), any())).then(SharingRoleServiceTest::callSecondArgument); + when(objectMapper.convertValue(payload, JsonNode.class)).thenReturn(createJsonNodeForRolePayload()); + + var expectedResponse = createSharingRoleResponse(createRolesPcId, updateRolesPcId); + var actualResponse = sharingRoleService.start(CONSORTIUM_ID, sharingRoleRequest); + + assertThat(actualResponse.getCreateRolesPCId()).isEqualTo(expectedResponse.getCreateRolesPCId()); + assertThat(actualResponse.getUpdateRolesPCId()).isEqualTo(expectedResponse.getUpdateRolesPCId()); + + verify(publicationService, times(2)).publishRequest(any(), any()); + } + + @Test + void shouldDeleteSharingRole() { + UUID pcId = UUID.randomUUID(); + UUID roleId = UUID.fromString("3844767a-8367-4926-9999-514c35840399"); + Tenant tenant1 = createTenant("tenant1", "tenant1"); + Tenant tenant2 = createTenant("tenant2", "tenant2"); + Set tenantAssociationsWithRole = Set.of("tenant1"); + TenantCollection tenantCollection = createTenantCollection(List.of(tenant1, tenant2)); + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + + // "tenant1" exists in tenant role association so that tenant1 is in DELETE request publication, + var expectedPublicationRequestDelete = createPublicationRequest(sharingRoleRequest, HttpMethod.DELETE.toString()); + expectedPublicationRequestDelete.setTenants(Set.of("tenant1")); + expectedPublicationRequestDelete.setUrl("/role/3844767a-8367-4926-9999-514c35840399"); + Map map = new LinkedHashMap<>(); + map.put("id", "3844767a-8367-4926-9999-514c35840399"); + map.put("name", "Role for policy: 104d7a66-c51d-402a-9c9f-3bdcdbbcdbe7"); + map.put("source", "local"); + expectedPublicationRequestDelete.setPayload(map); + + var publicationResponse = new PublicationResponse().id(pcId); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + when(sharingRoleRepository.existsByRoleId(roleId)).thenReturn(true); + when(publicationService.publishRequest(CONSORTIUM_ID, expectedPublicationRequestDelete)).thenReturn(publicationResponse); + when(tenantService.getAll(CONSORTIUM_ID)).thenReturn(tenantCollection); + when(sharingRoleRepository.findTenantsByRoleId(sharingRoleRequest.getRoleId())).thenReturn(tenantAssociationsWithRole); + when(folioExecutionContext.getTenantId()).thenReturn("mobius"); + when(systemUserScopedExecutionService.executeSystemUserScoped(eq("mobius"), any())).then(SharingRoleServiceTest::callSecondArgument); + + var expectedResponse = createSharingRoleResponseForDelete(pcId); + var actualResponse = sharingRoleService.delete(CONSORTIUM_ID, roleId, sharingRoleRequest); + + assertThat(actualResponse.getPcId()).isEqualTo(expectedResponse.getPcId()); + + verify(publicationService, times(1)).publishRequest(any(), any()); + } + + @Test + void shouldUpdateFailedTenantPolicies() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, JsonProcessingException { + UUID publicationId = UUID.randomUUID(); + UUID pcId = UUID.randomUUID(); + var publicationResponse = new PublicationResponse().id(pcId); + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + String centralTenant = "mobius"; + String localTenant = "school"; + var publicationResultCollection = createPublicationResultCollection(centralTenant, localTenant); + var publicationDetails = createPublicationDetails(PublicationStatus.ERROR); + JsonNode node = createJsonNodeForGroupPayload(); + // expected data for publish request + Set expectedFailedTenantList = new HashSet<>(List.of(centralTenant, localTenant)); + var expectedPublicationRequest = createExceptedPublicationRequest(sharingRoleRequest, expectedFailedTenantList, HttpMethod.PUT); + + // set time interval and maxTries for Thread sleep cycle + ReflectionTestUtils.setField(sharingRoleService, "maxTries", 60); + ReflectionTestUtils.setField(sharingRoleService, "interval", 200); + + when(publicationService.checkPublicationDetailsExists(CONSORTIUM_ID, publicationId)) + .thenReturn(false) + .thenReturn(false) + .thenReturn(true); + when(publicationService.getPublicationDetails(CONSORTIUM_ID, publicationId)).thenReturn(publicationDetails); + when(publicationService.getPublicationResults(CONSORTIUM_ID, publicationId)).thenReturn(publicationResultCollection); + when(objectMapper.convertValue(any(), eq(JsonNode.class))).thenReturn(node); + when(folioExecutionContext.getTenantId()).thenReturn("mobius"); + when(systemUserScopedExecutionService.executeSystemUserScoped(eq("mobius"), any())).then(SharingRoleServiceTest::callSecondArgument); + when(publicationService.publishRequest(CONSORTIUM_ID, expectedPublicationRequest)).thenReturn(publicationResponse); + + // Use reflection to access the protected method in BaseSharingService + Method method = SharingRoleService.class.getSuperclass().getDeclaredMethod("updateConfigsForFailedTenantsWithRetry", UUID.class, UUID.class, Object.class); + method.setAccessible(true); + method.invoke(sharingRoleService, CONSORTIUM_ID, publicationId, sharingRoleRequest); + + verify(publicationService).getPublicationDetails(CONSORTIUM_ID, publicationId); + verify(publicationService, times(3)).checkPublicationDetailsExists(CONSORTIUM_ID, publicationId); + verify(publicationService).publishRequest(CONSORTIUM_ID, expectedPublicationRequest); + } + + // Negative cases + @Test + void shouldThrowErrorForNotEqualRoleIdWithPayloadId() throws JsonProcessingException { + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + sharingRoleRequest.setRoleId(UUID.randomUUID()); + JsonNode node = createJsonNodeForRolePayload(); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + when(objectMapper.convertValue(any(), eq(JsonNode.class))).thenReturn(node); + + assertThrows(IllegalArgumentException.class, () -> sharingRoleService.start(CONSORTIUM_ID, sharingRoleRequest)); + verify(publicationService, times(0)).publishRequest(any(), any()); + } + + @Test + void shouldThrowErrorForNotEqualRoleIdPathId() { + UUID roleId = UUID.fromString("999999-8367-4926-9999-514c35840399"); + + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + + assertThrows(IllegalArgumentException.class, + () -> sharingRoleService.delete(CONSORTIUM_ID, roleId, sharingRoleRequest)); + verify(publicationService, times(0)).publishRequest(any(), any()); + } + + @Test + void shouldThrowErrorForNotHavingPayloadOfRole() { + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE_WITHOUT_PAYLOAD, SharingRoleRequest.class); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + + assertThrows(IllegalArgumentException.class, + () -> sharingRoleService.delete(CONSORTIUM_ID, sharingRoleRequest.getRoleId(), sharingRoleRequest)); + verify(publicationService, times(0)).publishRequest(any(), any()); + } + + @Test + void shouldThrowErrorForNotFound() { + UUID roleId = UUID.fromString("3844767a-8367-4926-9999-514c35840399"); + + var sharingRoleRequest = getMockDataObject(SHARING_ROLE_REQUEST_SAMPLE, SharingRoleRequest.class); + + when(consortiumRepository.existsById(CONSORTIUM_ID)).thenReturn(true); + when(sharingRoleRepository.existsByRoleId(roleId)).thenReturn(false); + + assertThrows(ResourceNotFoundException.class, + () -> sharingRoleService.delete(CONSORTIUM_ID, roleId, sharingRoleRequest)); + verify(publicationService, times(0)).publishRequest(any(), any()); + } + + public static PublicationRequest createExceptedPublicationRequest(SharingRoleRequest sharingRoleRequest, Set tenantList, HttpMethod method) { + var expectedPublicationRequest = new PublicationRequest(); + expectedPublicationRequest.setTenants(tenantList); + expectedPublicationRequest.setMethod(method.toString()); + expectedPublicationRequest.setUrl(sharingRoleRequest.getUrl() + "/" + sharingRoleRequest.getRoleId()); + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode root = mapper.createObjectNode(); + root.set("group", mapper.convertValue("space", JsonNode.class)); + root.set("source", mapper.convertValue("user", JsonNode.class)); + expectedPublicationRequest.setPayload(root); + return expectedPublicationRequest; + } + + private static T callSecondArgument(InvocationOnMock invocation) throws Exception { + var headers = Map.>of(XOkapiHeaders.TENANT, List.of("mobius")); + var context = new DefaultFolioExecutionContext(mock(FolioModuleMetadata.class), headers); + try(var ignored = new FolioExecutionContextSetter(context)) { + return invocation.>getArgument(1).call(); + } + } +} diff --git a/src/test/java/org/folio/consortia/support/EntityUtils.java b/src/test/java/org/folio/consortia/support/EntityUtils.java index ca00f432..2e20e36f 100644 --- a/src/test/java/org/folio/consortia/support/EntityUtils.java +++ b/src/test/java/org/folio/consortia/support/EntityUtils.java @@ -26,6 +26,9 @@ import org.folio.consortia.domain.dto.SharingPolicyDeleteResponse; import org.folio.consortia.domain.dto.SharingPolicyRequest; import org.folio.consortia.domain.dto.SharingPolicyResponse; +import org.folio.consortia.domain.dto.SharingRoleDeleteResponse; +import org.folio.consortia.domain.dto.SharingRoleRequest; +import org.folio.consortia.domain.dto.SharingRoleResponse; import org.folio.consortia.domain.dto.SharingSettingDeleteResponse; import org.folio.consortia.domain.dto.SharingSettingRequest; import org.folio.consortia.domain.dto.SharingSettingResponse; @@ -268,10 +271,21 @@ public static SharingPolicyResponse createSharingPolicyResponse(UUID createPolic return new SharingPolicyResponse().createPoliciesPCId(createPolicyPcId).updatePoliciesPCId(updatePolicyPcId); } + public static SharingPolicyDeleteResponse createSharingPolicyResponseForDelete(UUID pcId) { return new SharingPolicyDeleteResponse().pcId(pcId); } + public static SharingRoleResponse createSharingRoleResponse(UUID createRolePcId, UUID updateRolePcId) { + return new SharingRoleResponse().createRolesPCId(createRolePcId).updateRolesPCId(updateRolePcId); + } + + + public static SharingRoleDeleteResponse createSharingRoleResponseForDelete(UUID pcId) { + return new SharingRoleDeleteResponse().pcId(pcId); + } + + public static TenantCollection createTenantCollection(List tenants) { TenantCollection tenantCollection = new TenantCollection(); tenantCollection.setTenants(tenants); @@ -305,6 +319,19 @@ public static PublicationRequest createPublicationRequest(SharingPolicyRequest s return publicationRequest; } + public static PublicationRequest createPublicationRequest(SharingRoleRequest sharingRoleRequest, String method){ + PublicationRequest publicationRequest = new PublicationRequest(); + publicationRequest.setUrl(sharingRoleRequest.getUrl()); + publicationRequest.setMethod(method); + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode root = mapper.createObjectNode(); + root.set("id", mapper.convertValue("3844767a-8367-4926-9999-514c35840399", JsonNode.class)); + root.set("name", mapper.convertValue("Role for policy: 104d7a66-c51d-402a-9c9f-3bdcdbbcdbe7", JsonNode.class)); + root.set("source", mapper.convertValue("consortium", JsonNode.class)); + publicationRequest.setPayload(root); + return publicationRequest; + } + public static PublicationResultCollection createPublicationResultCollection(String tenantId1, String tenantId2) { var pbr1 = new PublicationResult(); pbr1.setTenantId(tenantId1); @@ -333,7 +360,7 @@ public static JsonNode createJsonNodeForDepartmentPayload() throws JsonProcessin return mapper.readTree(json); } - public static JsonNode createJsonNodeForRolePayload() throws JsonProcessingException { + public static JsonNode createJsonNodeForPolicyPayload() throws JsonProcessingException { Map payload = new HashMap<>(); payload.put("id", "2844767a-8367-4926-9999-514c35840399"); payload.put("name", "Policy for role: 004d7a66-c51d-402a-9c9f-3bdcdbbcdbe7"); @@ -343,6 +370,16 @@ public static JsonNode createJsonNodeForRolePayload() throws JsonProcessingExcep return mapper.readTree(json); } + public static JsonNode createJsonNodeForRolePayload() throws JsonProcessingException { + Map payload = new HashMap<>(); + payload.put("id", "3844767a-8367-4926-9999-514c35840399"); + payload.put("name", "Role for policy: 104d7a66-c51d-402a-9c9f-3bdcdbbcdbe7"); + payload.put("source", "local"); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(payload); + return mapper.readTree(json); + } + public static JsonNode createJsonNodeForGroupPayload() throws JsonProcessingException { Map payload = new HashMap<>(); payload.put("group", "space"); diff --git a/src/test/resources/mockdata/sharing_roles/sharing_role_request.json b/src/test/resources/mockdata/sharing_roles/sharing_role_request.json new file mode 100644 index 00000000..32554f83 --- /dev/null +++ b/src/test/resources/mockdata/sharing_roles/sharing_role_request.json @@ -0,0 +1,9 @@ +{ + "roleId": "3844767a-8367-4926-9999-514c35840399", + "url": "/role", + "payload": { + "id": "3844767a-8367-4926-9999-514c35840399", + "name": "Role for policy: 104d7a66-c51d-402a-9c9f-3bdcdbbcdbe7", + "source": "local" + } +} diff --git a/src/test/resources/mockdata/sharing_roles/sharing_role_request_without_payload.json b/src/test/resources/mockdata/sharing_roles/sharing_role_request_without_payload.json new file mode 100644 index 00000000..178989f4 --- /dev/null +++ b/src/test/resources/mockdata/sharing_roles/sharing_role_request_without_payload.json @@ -0,0 +1,4 @@ +{ + "roleId": "3844767a-8367-4926-9999-514c35840399", + "url": "/role" +}