diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index cdf86c6..936014b 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -12,7 +12,14 @@ ], "pathPattern": "/circulation-bff/requests/allowed-service-points", "permissionsRequired": ["circulation-bff.requests.allowed-service-points.get"], - "modulePermissions": [] + "modulePermissions": [ + "tlr.ecs-tlr-allowed-service-points.get", + "circulation.requests.allowed-service-points.get", + "tlr.settings.get", + "circulation.settings.item.get", + "circulation.settings.collection.get", + "user-tenants.collection.get" + ] }, { "methods": [ @@ -76,4 +83,4 @@ { "name": "JAVA_OPTIONS", "value": "-XX:MaxRAMPercentage=66.0"} ] } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/circulationbff/client/feign/CirculationClient.java b/src/main/java/org/folio/circulationbff/client/feign/CirculationClient.java new file mode 100644 index 0000000..fa47428 --- /dev/null +++ b/src/main/java/org/folio/circulationbff/client/feign/CirculationClient.java @@ -0,0 +1,20 @@ +package org.folio.circulationbff.client.feign; + +import org.folio.circulationbff.domain.dto.AllowedServicePointParams; +import org.folio.circulationbff.domain.dto.AllowedServicePoints; +import org.folio.circulationbff.domain.dto.CirculationSettingsResponse; +import org.folio.spring.config.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "circulation", url = "circulation", configuration = FeignClientConfiguration.class) +public interface CirculationClient { + + @GetMapping("/requests/allowed-service-points") + AllowedServicePoints allowedServicePoints (@SpringQueryMap AllowedServicePointParams params); + + @GetMapping(value = "/settings") + CirculationSettingsResponse getCirculationSettingsByQuery(@RequestParam("query") String query); +} diff --git a/src/main/java/org/folio/circulationbff/client/feign/EcsTlrClient.java b/src/main/java/org/folio/circulationbff/client/feign/EcsTlrClient.java new file mode 100644 index 0000000..746ce72 --- /dev/null +++ b/src/main/java/org/folio/circulationbff/client/feign/EcsTlrClient.java @@ -0,0 +1,20 @@ +package org.folio.circulationbff.client.feign; + +import org.folio.circulationbff.domain.dto.AllowedServicePointParams; +import org.folio.circulationbff.domain.dto.AllowedServicePoints; +import org.folio.circulationbff.domain.dto.TlrSettings; +import org.folio.spring.config.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient(name = "ecs-tlr", url = "tlr", configuration = FeignClientConfiguration.class) +public interface EcsTlrClient { + + @GetMapping("/allowed-service-points") + AllowedServicePoints getAllowedServicePoints(@SpringQueryMap AllowedServicePointParams params); + + @GetMapping("/settings") + TlrSettings getTlrSettings(); + +} diff --git a/src/main/java/org/folio/circulationbff/client/feign/UserTenantsClient.java b/src/main/java/org/folio/circulationbff/client/feign/UserTenantsClient.java new file mode 100644 index 0000000..f534d26 --- /dev/null +++ b/src/main/java/org/folio/circulationbff/client/feign/UserTenantsClient.java @@ -0,0 +1,15 @@ +package org.folio.circulationbff.client.feign; + + +import org.folio.circulationbff.domain.dto.UserTenantCollection; +import org.folio.spring.config.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "user-tenants", url = "user-tenants", configuration = FeignClientConfiguration.class) +public interface UserTenantsClient { + + @GetMapping() + UserTenantCollection getUserTenants(@RequestParam(name = "limit", required = false) Integer limit); +} diff --git a/src/main/java/org/folio/circulationbff/controller/CirculationBffController.java b/src/main/java/org/folio/circulationbff/controller/CirculationBffController.java index 324a3d9..b528964 100644 --- a/src/main/java/org/folio/circulationbff/controller/CirculationBffController.java +++ b/src/main/java/org/folio/circulationbff/controller/CirculationBffController.java @@ -2,9 +2,11 @@ import java.util.UUID; +import org.folio.circulationbff.domain.dto.AllowedServicePointParams; import org.folio.circulationbff.domain.dto.AllowedServicePoints; import org.folio.circulationbff.domain.dto.InstanceSearchResult; import org.folio.circulationbff.rest.resource.CirculationBffApi; +import org.folio.circulationbff.service.CirculationBffService; import org.folio.circulationbff.service.SearchService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,13 +20,28 @@ @RequiredArgsConstructor public class CirculationBffController implements CirculationBffApi { + private final CirculationBffService circulationBffService; private final SearchService searchService; @Override public ResponseEntity circulationBffRequestsAllowedServicePointsGet( - UUID patronGroupId, String operation, UUID instanceId, UUID requestId) { - - return ResponseEntity.status(HttpStatus.OK).body(new AllowedServicePoints()); + String operation, String tenantId, UUID patronGroupId, UUID instanceId, UUID requestId, + UUID requesterId, UUID itemId) { + + log.info("circulationBffRequestsAllowedServicePointsGet:: params: " + + "patronGroupId={}, operation={}, instanceId={}, requestId={}, requesterId={}, itemId={}", + patronGroupId, operation, instanceId, requestId, requesterId, itemId); + + return ResponseEntity.status(HttpStatus.OK).body(circulationBffService.getAllowedServicePoints( + AllowedServicePointParams.builder() + .operation(operation) + .patronGroupId(patronGroupId) + .instanceId(instanceId) + .requestId(requestId) + .requesterId(requestId) + .itemId(itemId) + .build(), + tenantId)); } @Override @@ -32,4 +49,4 @@ public ResponseEntity circulationBffRequestsSearchInstance return ResponseEntity.status(HttpStatus.OK) .body(searchService.findInstances(query)); } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/circulationbff/domain/dto/AllowedServicePointParams.java b/src/main/java/org/folio/circulationbff/domain/dto/AllowedServicePointParams.java new file mode 100644 index 0000000..8003345 --- /dev/null +++ b/src/main/java/org/folio/circulationbff/domain/dto/AllowedServicePointParams.java @@ -0,0 +1,22 @@ +package org.folio.circulationbff.domain.dto; + +import java.util.UUID; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class AllowedServicePointParams { + private String operation; + + private UUID patronGroupId; + + private UUID instanceId; + + private UUID requestId; + + private UUID requesterId; + + private UUID itemId; +} diff --git a/src/main/java/org/folio/circulationbff/service/CirculationBffService.java b/src/main/java/org/folio/circulationbff/service/CirculationBffService.java new file mode 100644 index 0000000..fc0bcdf --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/CirculationBffService.java @@ -0,0 +1,8 @@ +package org.folio.circulationbff.service; + +import org.folio.circulationbff.domain.dto.AllowedServicePointParams; +import org.folio.circulationbff.domain.dto.AllowedServicePoints; + +public interface CirculationBffService { + AllowedServicePoints getAllowedServicePoints(AllowedServicePointParams allowedServicePointParams, String tenantId); +} diff --git a/src/main/java/org/folio/circulationbff/service/SettingsService.java b/src/main/java/org/folio/circulationbff/service/SettingsService.java new file mode 100644 index 0000000..1c8b61d --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/SettingsService.java @@ -0,0 +1,5 @@ +package org.folio.circulationbff.service; + +public interface SettingsService { + boolean isEcsTlrFeatureEnabled(String tenantId); +} diff --git a/src/main/java/org/folio/circulationbff/service/UserTenantsService.java b/src/main/java/org/folio/circulationbff/service/UserTenantsService.java new file mode 100644 index 0000000..cb11378 --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/UserTenantsService.java @@ -0,0 +1,5 @@ +package org.folio.circulationbff.service; + +public interface UserTenantsService { + boolean isCentralTenant(String tenantId); +} diff --git a/src/main/java/org/folio/circulationbff/service/impl/CirculationBffServiceImpl.java b/src/main/java/org/folio/circulationbff/service/impl/CirculationBffServiceImpl.java new file mode 100644 index 0000000..4c8adaa --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/impl/CirculationBffServiceImpl.java @@ -0,0 +1,38 @@ +package org.folio.circulationbff.service.impl; + +import org.folio.circulationbff.client.feign.CirculationClient; +import org.folio.circulationbff.client.feign.EcsTlrClient; +import org.folio.circulationbff.domain.dto.AllowedServicePointParams; +import org.folio.circulationbff.domain.dto.AllowedServicePoints; +import org.folio.circulationbff.service.CirculationBffService; +import org.folio.circulationbff.service.SettingsService; +import org.folio.circulationbff.service.UserTenantsService; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class CirculationBffServiceImpl implements CirculationBffService { + + private final CirculationClient circulationClient; + private final EcsTlrClient ecsTlrClient; + private final SettingsService settingsService; + private final UserTenantsService userTenantsService; + + @Override + public AllowedServicePoints getAllowedServicePoints(AllowedServicePointParams params, String tenantId) { + log.info("getAllowedServicePoints:: params: {}", params); + if (settingsService.isEcsTlrFeatureEnabled(tenantId) && userTenantsService.isCentralTenant(tenantId)) { + log.info("getAllowedServicePoints:: Ecs TLR Feature is enabled. Getting allowed service " + + "points from mod-tlr module"); + return ecsTlrClient.getAllowedServicePoints(params); + } else { + log.info("getAllowedServicePoints:: Ecs TLR Feature is disabled. Getting allowed service " + + "points from mod-circulation module"); + return circulationClient.allowedServicePoints(params); + } + } +} diff --git a/src/main/java/org/folio/circulationbff/service/impl/SettingsServiceImpl.java b/src/main/java/org/folio/circulationbff/service/impl/SettingsServiceImpl.java new file mode 100644 index 0000000..fe22e1f --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/impl/SettingsServiceImpl.java @@ -0,0 +1,45 @@ +package org.folio.circulationbff.service.impl; + +import org.folio.circulationbff.client.feign.CirculationClient; +import org.folio.circulationbff.client.feign.EcsTlrClient; +import org.folio.circulationbff.service.SettingsService; +import org.folio.circulationbff.service.UserTenantsService; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class SettingsServiceImpl implements SettingsService { + + public static final String ECS_TLR_FEATURE_SETTINGS = "name=ecsTlrFeature"; + private final EcsTlrClient ecsTlrClient; + private final CirculationClient circulationClient; + private final UserTenantsService userTenantsService; + + @Override + public boolean isEcsTlrFeatureEnabled(String tenantId) { + if (userTenantsService.isCentralTenant(tenantId)) { + return ecsTlrClient.getTlrSettings().getEcsTlrFeatureEnabled(); + } + return isTlrEnabledInCirculationSettings(); + } + + private boolean isTlrEnabledInCirculationSettings() { + log.debug("getCirculationSettings:: Getting circulation settings"); + var circulationSettingsResponse = circulationClient.getCirculationSettingsByQuery(ECS_TLR_FEATURE_SETTINGS); + if (circulationSettingsResponse.getTotalRecords() > 0) { + try { + var circulationSettings = circulationSettingsResponse.getCirculationSettings().get(0); + log.info("getCirculationSettings:: circulation settings: {}", + circulationSettings.getValue()); + return circulationSettings.getValue().getEnabled(); + } catch (Exception e) { + log.error("getCirculationSettings:: Failed to parse circulation settings", e); + } + } + return false; + } +} diff --git a/src/main/java/org/folio/circulationbff/service/impl/UserTenantsServiceImpl.java b/src/main/java/org/folio/circulationbff/service/impl/UserTenantsServiceImpl.java new file mode 100644 index 0000000..187237f --- /dev/null +++ b/src/main/java/org/folio/circulationbff/service/impl/UserTenantsServiceImpl.java @@ -0,0 +1,53 @@ +package org.folio.circulationbff.service.impl; + +import java.util.List; + +import org.folio.circulationbff.client.feign.UserTenantsClient; +import org.folio.circulationbff.domain.dto.UserTenant; +import org.folio.circulationbff.domain.dto.UserTenantCollection; +import org.folio.circulationbff.service.UserTenantsService; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class UserTenantsServiceImpl implements UserTenantsService { + + private final UserTenantsClient userTenantsClient; + + @Override + public boolean isCentralTenant(String tenantId) { + UserTenant firstUserTenant = findFirstUserTenant(); + if (firstUserTenant == null) { + log.info("processUserGroupEvent: Failed to get user-tenants info"); + return false; + } + String centralTenantId = firstUserTenant.getCentralTenantId(); + if (centralTenantId != null && centralTenantId.equals(tenantId)) { + log.info("isCentralTenant: tenantId={} is central tenant", tenantId); + return true; + } + return false; + } + + private UserTenant findFirstUserTenant() { + log.info("findFirstUserTenant:: finding first userTenant"); + UserTenant firstUserTenant = null; + UserTenantCollection userTenantCollection = userTenantsClient.getUserTenants(1); + log.info("findFirstUserTenant:: userTenantCollection: {}", () -> userTenantCollection); + if (userTenantCollection != null) { + log.info("findFirstUserTenant:: userTenantCollection: {}", () -> userTenantCollection); + List userTenants = userTenantCollection.getUserTenants(); + if (!userTenants.isEmpty()) { + firstUserTenant = userTenants.get(0); + log.info("findFirstUserTenant:: found userTenant: {}", firstUserTenant); + } + } + log.info("findFirstUserTenant:: result: {}", firstUserTenant); + return firstUserTenant; + } +} + diff --git a/src/main/resources/swagger.api/circulation-bff.yaml b/src/main/resources/swagger.api/circulation-bff.yaml index edf4a09..7f66f55 100644 --- a/src/main/resources/swagger.api/circulation-bff.yaml +++ b/src/main/resources/swagger.api/circulation-bff.yaml @@ -7,3 +7,11 @@ paths: $ref: 'paths/allowedServicePoints/allowedServicePoints.yaml' /circulation-bff/requests/search-instances: $ref: 'paths/searchInstances/searchInstances.yaml' +components: + schemas: + tlr-settings: + $ref: 'schemas/dto/TlrSettings.yaml#/TlrSettings' + circulationSettingsResponse: + $ref: 'schemas/response/circulationSettingsResponse.yaml' + userTenantCollection: + $ref: 'schemas/dto/UserTenantCollection.yaml#/UserTenantCollection' diff --git a/src/main/resources/swagger.api/parameters/itemId.yaml b/src/main/resources/swagger.api/parameters/itemId.yaml new file mode 100644 index 0000000..d85219d --- /dev/null +++ b/src/main/resources/swagger.api/parameters/itemId.yaml @@ -0,0 +1,6 @@ +name: itemId +in: query +required: false +schema: + type: string + format: uuid diff --git a/src/main/resources/swagger.api/parameters/operation.yaml b/src/main/resources/swagger.api/parameters/operation.yaml index 26e0b9b..5453c6e 100644 --- a/src/main/resources/swagger.api/parameters/operation.yaml +++ b/src/main/resources/swagger.api/parameters/operation.yaml @@ -1,8 +1,8 @@ name: operation in: query -required: false +required: true schema: type: string enum: - create - - replace \ No newline at end of file + - replace diff --git a/src/main/resources/swagger.api/parameters/patronGroupId.yaml b/src/main/resources/swagger.api/parameters/patronGroupId.yaml index b5e6b2a..1faf680 100644 --- a/src/main/resources/swagger.api/parameters/patronGroupId.yaml +++ b/src/main/resources/swagger.api/parameters/patronGroupId.yaml @@ -1,6 +1,6 @@ name: patronGroupId in: query -required: true +required: false schema: type: string - format: uuid \ No newline at end of file + format: uuid diff --git a/src/main/resources/swagger.api/parameters/requesterId.yaml b/src/main/resources/swagger.api/parameters/requesterId.yaml new file mode 100644 index 0000000..72623d5 --- /dev/null +++ b/src/main/resources/swagger.api/parameters/requesterId.yaml @@ -0,0 +1,6 @@ +name: requesterId +in: query +required: false +schema: + type: string + format: uuid diff --git a/src/main/resources/swagger.api/paths/allowedServicePoints/allowedServicePoints.yaml b/src/main/resources/swagger.api/paths/allowedServicePoints/allowedServicePoints.yaml index edf97d4..2a2ae5e 100644 --- a/src/main/resources/swagger.api/paths/allowedServicePoints/allowedServicePoints.yaml +++ b/src/main/resources/swagger.api/paths/allowedServicePoints/allowedServicePoints.yaml @@ -5,6 +5,14 @@ get: - $ref: '../../parameters/patronGroupId.yaml' - $ref: '../../parameters/instanceId.yaml' - $ref: '../../parameters/requestId.yaml' + - $ref: '../../parameters/requesterId.yaml' + - $ref: '../../parameters/itemId.yaml' + - in: header + name: X-Okapi-Tenant + required: true + schema: + type: string + description: The tenant ID for the request tags: - getAllowedServicePoints responses: @@ -19,4 +27,4 @@ get: '422': $ref: '../../responses/unprocessableEntityResponse.yaml' '500': - $ref: '../../responses/internalServerErrorResponse.yaml' \ No newline at end of file + $ref: '../../responses/internalServerErrorResponse.yaml' diff --git a/src/main/resources/swagger.api/schemas/dto/CirculationSettings.yaml b/src/main/resources/swagger.api/schemas/dto/CirculationSettings.yaml new file mode 100644 index 0000000..3d1a8d6 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/dto/CirculationSettings.yaml @@ -0,0 +1,21 @@ +CirculationSettings: + description: Circulation Settings in a data tenant + type: "object" + properties: + id: + description: "ID of the Circulation Settings" + $ref: "common/uuid.yaml" + name: + description: "Name of the Circulation Settings" + $ref: "common/uuid.yaml" + value: + description: "Value of the Circulation Settings" + type: object + properties: + enabled: + description: "Whether the setting is enabled" + type: boolean + required: + - id + - name + - value diff --git a/src/main/resources/swagger.api/schemas/dto/TlrSettings.yaml b/src/main/resources/swagger.api/schemas/dto/TlrSettings.yaml new file mode 100644 index 0000000..aad4ecd --- /dev/null +++ b/src/main/resources/swagger.api/schemas/dto/TlrSettings.yaml @@ -0,0 +1,12 @@ +TlrSettings: + description: TLR Settings in a multi-tenant environment with Сonsortia support enabled + type: "object" + properties: + id: + description: "ID of the ECS TLR Settings" + $ref: "common/uuid.yaml" + ecsTlrFeatureEnabled: + description: "Indicates if TLR feature is enabled" + type: boolean + required: + - ecsTlrFeatureEnabled diff --git a/src/main/resources/swagger.api/schemas/dto/UserTenant.yaml b/src/main/resources/swagger.api/schemas/dto/UserTenant.yaml new file mode 100644 index 0000000..5fe37d4 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/dto/UserTenant.yaml @@ -0,0 +1,41 @@ +UserTenant: + description: Primary tenant of a user used for single-sign-on + type: "object" + properties: + id: + description: "UUID of the user tenant" + $ref: "common/uuid.yaml" + userId: + description: "UUID of the user" + $ref: "common/uuid.yaml" + username: + description: "The user name" + type: string + tenantId: + description: "Primary tenant of the user for single-sign-on" + type: string + centralTenantId: + description: "Central tenant id in the consortium" + type: string + phoneNumber: + description: "The user's primary phone number" + type: string + mobilePhoneNumber: + description: "The user's mobile phone number" + type: string + email: + description: "The user's email address" + type: string + barcode: + description: "The barcode of the user's" + type: string + externalSystemId: + description: "The externalSystemId of the user's" + type: string + consortiumId: + description: "UUID of the consortiumId" + $ref: "common/uuid.yaml" + additionalProperties: true + required: + - userId + - tenantId diff --git a/src/main/resources/swagger.api/schemas/dto/UserTenantCollection.yaml b/src/main/resources/swagger.api/schemas/dto/UserTenantCollection.yaml new file mode 100644 index 0000000..494f2ab --- /dev/null +++ b/src/main/resources/swagger.api/schemas/dto/UserTenantCollection.yaml @@ -0,0 +1,17 @@ +UserTenantCollection: + type: object + description: Collection of primary tenant records + properties: + userTenants: + description: List of primary tenant records + type: array + id: userTenants + items: + type: object + $ref: "UserTenant.yaml#/UserTenant" + totalRecords: + type: integer + additionalProperties: true + required: + - userTenants + - totalRecords diff --git a/src/main/resources/swagger.api/schemas/response/circulationSettingsResponse.yaml b/src/main/resources/swagger.api/schemas/response/circulationSettingsResponse.yaml new file mode 100644 index 0000000..137a18d --- /dev/null +++ b/src/main/resources/swagger.api/schemas/response/circulationSettingsResponse.yaml @@ -0,0 +1,11 @@ +description: "Circulation settings search result response" +type: "object" +properties: + totalRecords: + type: "integer" + description: "Amount of settings found" + circulationSettings: + type: "array" + description: "List of settings found" + items: + $ref: "../dto/CirculationSettings.yaml#/CirculationSettings" diff --git a/src/test/java/org/folio/circulationbff/api/BaseIT.java b/src/test/java/org/folio/circulationbff/api/BaseIT.java index cdc3591..4cd2b91 100644 --- a/src/test/java/org/folio/circulationbff/api/BaseIT.java +++ b/src/test/java/org/folio/circulationbff/api/BaseIT.java @@ -3,6 +3,8 @@ import static java.util.stream.Collectors.toMap; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.util.TestSocketUtils.findAvailableTcpPort; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.Collection; import java.util.HashMap; @@ -12,6 +14,7 @@ import org.folio.spring.FolioModuleMetadata; import org.folio.spring.integration.XOkapiHeaders; import org.folio.spring.scope.FolioExecutionContextSetter; +import org.folio.tenant.domain.dto.TenantAttributes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -24,18 +27,34 @@ import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.github.tomakehurst.wiremock.WireMockServer; +import lombok.SneakyThrows; + @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class BaseIT { + protected static final String HEADER_TENANT = "x-okapi-tenant"; protected static final String TOKEN = "test_token"; protected static final String TENANT_ID_CONSORTIUM = "consortium"; + protected static final String TENANT_ID_COLLEGE = "college"; protected static final String USER_ID = randomId(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + private FolioExecutionContextSetter contextSetter; @Autowired protected MockMvc mockMvc; + @Autowired private FolioModuleMetadata moduleMetadata; @@ -45,17 +64,19 @@ public class BaseIT { } @BeforeAll - static void beforeAll() { + static void beforeAll(@Autowired MockMvc mockMvc) { + setUpTenant(mockMvc); } @BeforeEach void beforeEachTest() { - initFolioContext(); + contextSetter = initFolioContext(); wireMockServer.resetAll(); } @AfterEach public void afterEachTest() { + contextSetter.close(); } @DynamicPropertySource @@ -63,12 +84,25 @@ static void overrideProperties(DynamicPropertyRegistry registry) { registry.add("folio.okapi-url", wireMockServer::baseUrl); } + @SneakyThrows + protected static void setUpTenant(MockMvc mockMvc) { + mockMvc.perform(post("/_/tenant") + .content(asJsonString(new TenantAttributes().moduleTo("mod-requests-mediated"))) + .headers(defaultHeaders()) + .contentType(APPLICATION_JSON)).andExpect(status().isNoContent()); + } + + public static HttpHeaders buildHeaders(String tenantId) { + HttpHeaders headers = defaultHeaders(); + headers.set(XOkapiHeaders.TENANT, tenantId); + return headers; + } + public static HttpHeaders defaultHeaders() { final HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(APPLICATION_JSON); httpHeaders.add(XOkapiHeaders.TENANT, TENANT_ID_CONSORTIUM); - httpHeaders.add(XOkapiHeaders.URL, (wireMockServer.baseUrl())); + httpHeaders.add(XOkapiHeaders.URL, wireMockServer.baseUrl()); httpHeaders.add(XOkapiHeaders.TOKEN, TOKEN); httpHeaders.add(XOkapiHeaders.USER_ID, USER_ID); @@ -87,4 +121,9 @@ protected static String randomId() { return UUID.randomUUID().toString(); } -} \ No newline at end of file + @SneakyThrows + public static String asJsonString(Object value) { + return OBJECT_MAPPER.writeValueAsString(value); + } + +} diff --git a/src/test/java/org/folio/circulationbff/api/CirculationBffRequestsApiTest.java b/src/test/java/org/folio/circulationbff/api/CirculationBffRequestsApiTest.java index 86021f1..619d6a4 100644 --- a/src/test/java/org/folio/circulationbff/api/CirculationBffRequestsApiTest.java +++ b/src/test/java/org/folio/circulationbff/api/CirculationBffRequestsApiTest.java @@ -1,42 +1,136 @@ package org.folio.circulationbff.api; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static java.lang.String.format; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.List; +import java.util.Set; +import java.util.UUID; + import org.apache.http.HttpStatus; +import org.folio.circulationbff.domain.dto.AllowedServicePoints; +import org.folio.circulationbff.domain.dto.AllowedServicePoints1Inner; +import org.folio.circulationbff.domain.dto.CirculationSettings; +import org.folio.circulationbff.domain.dto.CirculationSettingsResponse; +import org.folio.circulationbff.domain.dto.CirculationSettingsValue; import org.folio.circulationbff.domain.dto.Instance; import org.folio.circulationbff.domain.dto.InstanceSearchResult; +import org.folio.circulationbff.domain.dto.TlrSettings; +import org.folio.circulationbff.domain.dto.UserTenant; +import org.folio.circulationbff.domain.dto.UserTenantCollection; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import com.github.tomakehurst.wiremock.client.WireMock; + +import lombok.SneakyThrows; + class CirculationBffRequestsApiTest extends BaseIT { - private static final String ALLOWED_SP_URL_PATH = - "/circulation-bff/requests/allowed-service-points"; - private static final String ALLOWED_SP_QUERY_PARAM_TMP = "operation=%s&instanceId=%s&patronGroupId=%s"; private static final String SEARCH_INSTANCES_URL_PATH = "/circulation-bff/requests/search-instances"; private static final String SEARCH_INSTANCES_MOD_SEARCH_URL_PATH = "/search/instances"; - private static final String URL_TMP = "%s?%s"; + private static final String ALLOWED_SERVICE_POINT_PATH = "/circulation-bff/requests/allowed" + + "-service-points"; + private static final String CIRCULATION_SETTINGS_URL = "/circulation/settings"; + private static final String TLR_SETTINGS_URL = "/tlr/settings"; + private static final String USER_TENANTS_URL = "/user-tenants"; + private static final String TLR_ALLOWED_SERVICE_POINT_URL = "/tlr/allowed-service-points"; + private static final String CIRCULATION_ALLOWED_SERVICE_POINT_URL = "/circulation/requests" + + "/allowed-service-points"; + + @Test + @SneakyThrows + void callsModTlrWhenEcsTlrEnabledInCentralTenant() { + var userTenant = new UserTenant(UUID.randomUUID().toString(), TENANT_ID_CONSORTIUM); + userTenant.setCentralTenantId(TENANT_ID_CONSORTIUM); + mockUserTenants(userTenant, TENANT_ID_CONSORTIUM); + mockEcsTlrSettings(true); + mockAllowedServicePoints(TENANT_ID_CONSORTIUM); + + var operation = "create"; + var instanceId = UUID.randomUUID(); + var requestId = UUID.randomUUID(); + var patronGroupId = UUID.randomUUID(); + + var allowedSpResponseConsortium = new AllowedServicePoints(); + allowedSpResponseConsortium.setHold(Set.of( + buildAllowedServicePoint("SP_consortium_1"), + buildAllowedServicePoint("SP_consortium_2"))); + wireMockServer.stubFor(WireMock.get(urlPathEqualTo(TLR_ALLOWED_SERVICE_POINT_URL)) + .withHeader(HEADER_TENANT, equalTo(TENANT_ID_CONSORTIUM)) + .willReturn(jsonResponse(asJsonString(allowedSpResponseConsortium), SC_OK))); + + mockMvc.perform( + get(ALLOWED_SERVICE_POINT_PATH) + .queryParam("operation", "create") + .queryParam("requestId", requestId.toString()) + .queryParam("instanceId", instanceId.toString()) + .queryParam("patronGroupId", patronGroupId.toString()) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.Page").doesNotExist()) + .andExpect(jsonPath("$.Hold").exists()) + .andExpect(jsonPath("$.Recall").doesNotExist()) + .andExpect(jsonPath("$.Hold[*].name", containsInAnyOrder("SP_consortium_1", "SP_consortium_2"))); + + wireMockServer.verify(getRequestedFor(urlPathEqualTo(TLR_ALLOWED_SERVICE_POINT_URL)) + .withQueryParam("requestId", equalTo(requestId.toString())) + .withQueryParam("instanceId", equalTo(instanceId.toString())) + .withQueryParam("operation", equalTo(operation)) + ); + } @Test - void allowedServicePointsReturnsOkStatus() throws Exception { + @SneakyThrows + void callsCirculationWhenEcsTlrDisabledOnDataTenant() { + var userTenant = new UserTenant(UUID.randomUUID().toString(), TENANT_ID_COLLEGE); + userTenant.setCentralTenantId(TENANT_ID_CONSORTIUM); + mockUserTenants(userTenant, TENANT_ID_COLLEGE); + mockEcsTlrCirculationSettings(true); + mockAllowedServicePoints(TENANT_ID_COLLEGE); + + var operation = "create"; + var instanceId = UUID.randomUUID(); + var requestId = UUID.randomUUID(); + var patronGroupId = UUID.randomUUID(); + mockMvc.perform( - get(buildUrl(ALLOWED_SP_URL_PATH, ALLOWED_SP_QUERY_PARAM_TMP, "create", randomId(), - randomId())) - .headers(defaultHeaders()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); + get(ALLOWED_SERVICE_POINT_PATH) + .queryParam("operation", "create") + .queryParam("requestId", requestId.toString()) + .queryParam("instanceId", instanceId.toString()) + .queryParam("patronGroupId", patronGroupId.toString()) + .headers(buildHeaders(TENANT_ID_COLLEGE)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.Page").doesNotExist()) + .andExpect(jsonPath("$.Hold").exists()) + .andExpect(jsonPath("$.Recall").doesNotExist()) + .andExpect(jsonPath("$.Hold[*].name", containsInAnyOrder("SP_consortium_1", "SP_consortium_2"))); + + wireMockServer.verify(getRequestedFor(urlPathEqualTo( + CIRCULATION_ALLOWED_SERVICE_POINT_URL)) + .withQueryParam("requestId", equalTo(requestId.toString())) + .withQueryParam("instanceId", equalTo(instanceId.toString())) + .withQueryParam("operation", equalTo(operation)) + ); } @Test - void searchInstancesReturnsOkStatus() throws Exception { + @SneakyThrows + void searchInstancesReturnsOkStatus() { String instanceId = randomId(); InstanceSearchResult mockSearchResponse = new InstanceSearchResult() .addInstancesItem(new Instance().id(instanceId)) @@ -48,16 +142,59 @@ void searchInstancesReturnsOkStatus() throws Exception { .willReturn(jsonResponse(mockSearchResponse, HttpStatus.SC_OK))); mockMvc.perform( - get(SEARCH_INSTANCES_URL_PATH) - .queryParam("query", "id==" + instanceId) - .headers(defaultHeaders()) - .contentType(MediaType.APPLICATION_JSON)) + get(SEARCH_INSTANCES_URL_PATH) + .queryParam("query", "id==" + instanceId) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("instances[0].id", is(instanceId))) .andExpect(jsonPath("totalRecords", is(1))); } - private String buildUrl(String path, String parametersTemplate, String... parameters) { - return format(URL_TMP, path, format(parametersTemplate, (Object[]) parameters)); + private AllowedServicePoints1Inner buildAllowedServicePoint(String name) { + return new AllowedServicePoints1Inner() + .id(randomId()) + .name(name); + } + + private void mockUserTenants(UserTenant userTenant, String requestTenant) { + wireMockServer.stubFor(WireMock.get(urlPathEqualTo(USER_TENANTS_URL)) + .withQueryParam("limit", matching("\\d*")) + .withHeader(HEADER_TENANT, equalTo(requestTenant)) + .willReturn(jsonResponse(asJsonString(new UserTenantCollection().addUserTenantsItem(userTenant)), + SC_OK))); + } + + private void mockEcsTlrCirculationSettings(boolean enabled) { + var circulationSettingsResponse = new CirculationSettingsResponse(); + circulationSettingsResponse.setTotalRecords(1); + circulationSettingsResponse.setCirculationSettings(List.of( + new CirculationSettings() + .name("ecsTlrFeature") + .value(new CirculationSettingsValue().enabled(enabled)) + )); + wireMockServer.stubFor(WireMock.get(urlPathEqualTo(CIRCULATION_SETTINGS_URL)) + .withQueryParam("query", equalTo("name=ecsTlrFeature")) + .withHeader(HEADER_TENANT, equalTo(TENANT_ID_COLLEGE)) + .willReturn(jsonResponse(asJsonString(circulationSettingsResponse), + SC_OK))); + } + + private void mockEcsTlrSettings(boolean enabled) { + TlrSettings tlrSettings = new TlrSettings(); + tlrSettings.setEcsTlrFeatureEnabled(enabled); + wireMockServer.stubFor(WireMock.get(urlMatching(TLR_SETTINGS_URL)) + .withHeader(HEADER_TENANT, equalTo(TENANT_ID_CONSORTIUM)) + .willReturn(jsonResponse(asJsonString(tlrSettings), SC_OK))); + } + + private void mockAllowedServicePoints(String requestTenant) { + var allowedSpResponseConsortium = new AllowedServicePoints(); + allowedSpResponseConsortium.setHold(Set.of( + buildAllowedServicePoint("SP_consortium_1"), + buildAllowedServicePoint("SP_consortium_2"))); + wireMockServer.stubFor(WireMock.get(urlPathEqualTo(CIRCULATION_ALLOWED_SERVICE_POINT_URL)) + .withHeader(HEADER_TENANT, equalTo(requestTenant)) + .willReturn(jsonResponse(asJsonString(allowedSpResponseConsortium), SC_OK))); } -} \ No newline at end of file +} diff --git a/src/test/java/org/folio/circulationbff/util/TestUtils.java b/src/test/java/org/folio/circulationbff/util/TestUtils.java new file mode 100644 index 0000000..4be572d --- /dev/null +++ b/src/test/java/org/folio/circulationbff/util/TestUtils.java @@ -0,0 +1,33 @@ +package org.folio.circulationbff.util; + +import java.util.Base64; + +import org.json.JSONObject; + +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class TestUtils { + + @SneakyThrows + public static String buildToken(String tenantId) { + JSONObject header = new JSONObject() + .put("alg", "HS256"); + + JSONObject payload = new JSONObject() + .put("sub", tenantId + "_admin") + .put("user_id", "bb6a6f19-9275-4261-ad9d-6c178c24c4fb") + .put("type", "access") + .put("exp", 1708342543) + .put("iat", 1708341943) + .put("tenant", tenantId); + + String signature = "De_0um7P_Rv-diqjHKLcSHZdjzjjshvlBbi6QPrz0Tw"; + + return String.format("%s.%s.%s", + Base64.getEncoder().encodeToString(header.toString().getBytes()), + Base64.getEncoder().encodeToString(payload.toString().getBytes()), + signature); + } +}