diff --git a/NEWS.md b/NEWS.md index 4c93f2c33..fbeb65c7b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ * Provides `indices v0.7` * Provides `search v1.3` * Requires `locations v3.0` +* Provides `consortium-search v1.1` ### Features * Create location index and process location events ([MSEARCH-703](https://issues.folio.org/browse/MSEARCH-703)) @@ -14,6 +15,7 @@ * Instance search: add search option that search instances by normalized classification number ([MSEARCH-697](https://issues.folio.org/browse/MSEARCH-697)) * Instance search: make "all" search field option to search by full-text fields ([MSEARCH-606](https://issues.folio.org/browse/MSEARCH-606)) * Facets: add support for instance classification facets ([MSEARCH-606](https://issues.folio.org/browse/MSEARCH-606)) +* Return Unified List of Inventory Locations in a Consortium ([MSEARCH-681](https://folio-org.atlassian.net/browse/MSEARCH-681)) ### Bug fixes * Do not delete kafka topics if collection topic is enabled ([MSEARCH-725](https://folio-org.atlassian.net/browse/MSEARCH-725)) diff --git a/README.md b/README.md index a901bc875..3ec557d78 100644 --- a/README.md +++ b/README.md @@ -862,10 +862,11 @@ can't receive ids by query. ### Consortium Search API Special API that provide consolidated access to records in consortium environment. Works only for central tenant. -| METHOD | URL | DESCRIPTION | -|:-------|:------------------------------|:------------------------------| -| GET | `/search/consortium/holdings` | Returns consolidated holdings | -| GET | `/search/consortium/items` | Returns consolidated items | +| METHOD | URL | DESCRIPTION | +|:-------|:-------------------------------|:-------------------------------| +| GET | `/search/consortium/holdings` | Returns consolidated holdings | +| GET | `/search/consortium/items` | Returns consolidated items | +| GET | `/search/consortium/locations` | Returns consolidated locations | ## Additional Information diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 71977ab08..5087b9c57 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -142,7 +142,7 @@ }, { "id": "consortium-search", - "version": "1.0", + "version": "1.1", "handlers": [ { "methods": [ @@ -167,6 +167,18 @@ "modulePermissions": [ "user-tenants.collection.get" ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/search/consortium/locations", + "permissionsRequired": [ + "consortium-search.locations.collection.get" + ], + "modulePermissions": [ + "user-tenants.collection.get" + ] } ] }, @@ -635,6 +647,11 @@ "permissionName": "consortium-search.items.collection.get", "displayName": "Consortium Search - fetch items records", "description": "Returns items records in consortium" + }, + { + "permissionName": "consortium-search.locations.collection.get", + "displayName": "Consortium Search - fetch locations records", + "description": "Returns location records in consortium" } ], "launchDescriptor": { diff --git a/src/main/java/org/folio/search/controller/SearchConsortiumController.java b/src/main/java/org/folio/search/controller/SearchConsortiumController.java index 69f77425e..7d0fc85ba 100644 --- a/src/main/java/org/folio/search/controller/SearchConsortiumController.java +++ b/src/main/java/org/folio/search/controller/SearchConsortiumController.java @@ -3,12 +3,14 @@ import lombok.RequiredArgsConstructor; import org.folio.search.domain.dto.ConsortiumHoldingCollection; import org.folio.search.domain.dto.ConsortiumItemCollection; +import org.folio.search.domain.dto.ConsortiumLocationCollection; import org.folio.search.domain.dto.SortOrder; import org.folio.search.exception.RequestValidationException; import org.folio.search.model.service.ConsortiumSearchContext; import org.folio.search.model.types.ResourceType; import org.folio.search.rest.resource.SearchConsortiumApi; import org.folio.search.service.consortium.ConsortiumInstanceService; +import org.folio.search.service.consortium.ConsortiumLocationService; import org.folio.search.service.consortium.ConsortiumTenantService; import org.folio.spring.integration.XOkapiHeaders; import org.springframework.http.ResponseEntity; @@ -27,6 +29,7 @@ public class SearchConsortiumController implements SearchConsortiumApi { private final ConsortiumTenantService consortiumTenantService; private final ConsortiumInstanceService instanceService; + private final ConsortiumLocationService locationService; @Override public ResponseEntity getConsortiumHoldings(String tenantHeader, String instanceId, @@ -63,6 +66,22 @@ public ResponseEntity getConsortiumItems(String tenant return ResponseEntity.ok(instanceService.fetchItems(context)); } + @Override + public ResponseEntity getConsortiumLocations(String tenantHeader, + String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + checkAllowance(tenantHeader); + var result = locationService.fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + + return ResponseEntity.ok(new + ConsortiumLocationCollection() + .locations(result.getRecords()) + .totalRecords(result.getTotalRecords())); + } + private void checkAllowance(String tenantHeader) { var centralTenant = consortiumTenantService.getCentralTenant(tenantHeader); if (centralTenant.isEmpty() || !centralTenant.get().equals(tenantHeader)) { diff --git a/src/main/java/org/folio/search/repository/ConsortiumLocationRepository.java b/src/main/java/org/folio/search/repository/ConsortiumLocationRepository.java new file mode 100644 index 000000000..5927e1391 --- /dev/null +++ b/src/main/java/org/folio/search/repository/ConsortiumLocationRepository.java @@ -0,0 +1,78 @@ +package org.folio.search.repository; + +import static org.folio.search.utils.SearchUtils.TENANT_ID_FIELD_NAME; +import static org.folio.search.utils.SearchUtils.performExceptionalOperation; +import static org.opensearch.search.sort.SortOrder.ASC; +import static org.opensearch.search.sort.SortOrder.DESC; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.search.domain.dto.ConsortiumLocation; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.SearchResult; +import org.folio.search.service.converter.ElasticsearchDocumentConverter; +import org.jetbrains.annotations.NotNull; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.search.sort.SortBuilders; +import org.springframework.stereotype.Repository; + + +@Log4j2 +@Repository +@RequiredArgsConstructor +public class ConsortiumLocationRepository { + + public static final String LOCATION_INDEX = "location"; + private static final String OPERATION_TYPE = "searchApi"; + private final IndexNameProvider indexNameProvider; + private final ElasticsearchDocumentConverter documentConverter; + + private final RestHighLevelClient client; + + public SearchResult fetchLocations(String tenantHeader, + String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + + var sourceBuilder = getSearchSourceBuilder(tenantId, limit, offset, sortBy, sortOrder); + var response = search(sourceBuilder, tenantHeader); + return documentConverter.convertToSearchResult(response, ConsortiumLocation.class); + } + + @NotNull + private static SearchSourceBuilder getSearchSourceBuilder(String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + var sourceBuilder = new SearchSourceBuilder(); + Optional.ofNullable(tenantId) + .ifPresent(id -> sourceBuilder + .query(QueryBuilders + .termQuery(TENANT_ID_FIELD_NAME, id))); + + return sourceBuilder + .from(offset) + .sort(SortBuilders + .fieldSort(sortBy) + .order(sortOrder == SortOrder.DESC ? DESC : ASC)) + .size(limit); + } + + private SearchResponse search(SearchSourceBuilder sourceBuilder, String tenantHeader) { + var index = indexNameProvider.getIndexName(LOCATION_INDEX, tenantHeader); + var searchRequest = new SearchRequest(index); + searchRequest.source(sourceBuilder); + return performExceptionalOperation(() -> client.search(searchRequest, + RequestOptions.DEFAULT), index, OPERATION_TYPE); + } + +} diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumLocationService.java b/src/main/java/org/folio/search/service/consortium/ConsortiumLocationService.java new file mode 100644 index 000000000..19d44b60e --- /dev/null +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumLocationService.java @@ -0,0 +1,42 @@ +package org.folio.search.service.consortium; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.search.domain.dto.ConsortiumLocation; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.SearchResult; +import org.folio.search.repository.ConsortiumLocationRepository; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ConsortiumLocationService { + + public static final String NAME = "name"; + public static final String ID = "id"; + public static final String TENANT_ID = "tenantId"; + private final ConsortiumLocationRepository repository; + + public SearchResult fetchLocations(String tenantHeader, + String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + log.info("fetching consortium locations for tenant: {}, tenantId: {}, sortBy: {}", + tenantHeader, + tenantId, + sortBy); + validateSortByValue(sortBy); + return repository.fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + } + + private void validateSortByValue(String sortBy) { + if (!(NAME.equals(sortBy) || ID.equals(sortBy) || TENANT_ID.equals(sortBy))) { + throw new IllegalArgumentException("Invalid sortBy value: " + sortBy); + } + } + + +} diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml index 5917f5976..d0c347cff 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -73,6 +73,9 @@ paths: /search/consortium/items: $ref: 'paths/search-consortium/search-consortium-items.yaml' + /search/consortium/locations: + $ref: 'paths/search-consortium/search-consortium-locations.yaml' + /search/index/indices: $ref: 'paths/search-index/search-index-indices.yaml' diff --git a/src/main/resources/swagger.api/parameters/consortium-locations-limit-param.yaml b/src/main/resources/swagger.api/parameters/consortium-locations-limit-param.yaml new file mode 100644 index 000000000..aa4edf38b --- /dev/null +++ b/src/main/resources/swagger.api/parameters/consortium-locations-limit-param.yaml @@ -0,0 +1,8 @@ +in: query +name: limit +description: Limit the number of elements returned in the response. +schema: + type: integer + minimum: 0 + maximum: 1000 + default: 1000 diff --git a/src/main/resources/swagger.api/parameters/sort-by-location-name-param.yaml b/src/main/resources/swagger.api/parameters/sort-by-location-name-param.yaml new file mode 100644 index 000000000..69cb5d29a --- /dev/null +++ b/src/main/resources/swagger.api/parameters/sort-by-location-name-param.yaml @@ -0,0 +1,12 @@ +in: query +name: sortBy +description: | + Defines a field to sort by. + Possible values: + - id + - tenantId + - name +required: false +schema: + type: string + default: name diff --git a/src/main/resources/swagger.api/paths/search-consortium/search-consortium-locations.yaml b/src/main/resources/swagger.api/paths/search-consortium/search-consortium-locations.yaml new file mode 100644 index 000000000..5f6aee61a --- /dev/null +++ b/src/main/resources/swagger.api/paths/search-consortium/search-consortium-locations.yaml @@ -0,0 +1,24 @@ +get: + operationId: getConsortiumLocations + summary: Get Consortium Locations + description: Get a list of locations (only for consortium environment) + tags: + - search-consortium + parameters: + - $ref: '../../parameters/tenant-id-query-param.yaml' + - $ref: '../../parameters/consortium-locations-limit-param.yaml' + - $ref: '../../parameters/offset-param.yaml' + - $ref: '../../parameters/sort-by-location-name-param.yaml' + - $ref: '../../parameters/sort-order-param.yaml' + - $ref: '../../parameters/x-okapi-tenant-header.yaml' + responses: + '200': + description: List of locations + content: + application/json: + schema: + $ref: '../../schemas/entity/consortiumLocationCollection.yaml' + '400': + $ref: '../../responses/badRequestResponse.yaml' + '500': + $ref: '../../responses/internalServerErrorResponse.yaml' diff --git a/src/main/resources/swagger.api/schemas/entity/consortiumLocation.yaml b/src/main/resources/swagger.api/schemas/entity/consortiumLocation.yaml new file mode 100644 index 000000000..d3938d42f --- /dev/null +++ b/src/main/resources/swagger.api/schemas/entity/consortiumLocation.yaml @@ -0,0 +1,11 @@ +type: object +properties: + id: + description: Location ID + type: string + name: + description: Location name + type: string + tenantId: + description: Tenant ID of the Location + type: string diff --git a/src/main/resources/swagger.api/schemas/entity/consortiumLocationCollection.yaml b/src/main/resources/swagger.api/schemas/entity/consortiumLocationCollection.yaml new file mode 100644 index 000000000..cb7c417f5 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/entity/consortiumLocationCollection.yaml @@ -0,0 +1,8 @@ +type: object +properties: + locations: + type: array + items: + $ref: './consortiumLocation.yaml' + totalRecords: + type: integer diff --git a/src/test/java/org/folio/search/controller/ConsortiumSearchLocationsIT.java b/src/test/java/org/folio/search/controller/ConsortiumSearchLocationsIT.java new file mode 100644 index 000000000..2cd5a2ba8 --- /dev/null +++ b/src/test/java/org/folio/search/controller/ConsortiumSearchLocationsIT.java @@ -0,0 +1,90 @@ +package org.folio.search.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Durations.ONE_MINUTE; +import static org.awaitility.Durations.ONE_SECOND; +import static org.folio.search.domain.dto.ResourceEventType.CREATE; +import static org.folio.search.model.Pair.pair; +import static org.folio.search.sample.SampleLocations.getLocationsSampleAsMap; +import static org.folio.search.support.base.ApiEndpoints.consortiumLocationsSearchPath; +import static org.folio.search.utils.SearchUtils.LOCATION_RESOURCE; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; +import static org.folio.search.utils.TestConstants.MEMBER_TENANT_ID; +import static org.folio.search.utils.TestConstants.inventoryLocationTopic; +import static org.folio.search.utils.TestUtils.kafkaResourceEvent; +import static org.folio.search.utils.TestUtils.parseResponse; + +import java.util.List; +import org.folio.search.domain.dto.ConsortiumLocationCollection; +import org.folio.search.model.Pair; +import org.folio.search.support.base.BaseConsortiumIntegrationTest; +import org.folio.spring.testing.type.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@IntegrationTest +class ConsortiumSearchLocationsIT extends BaseConsortiumIntegrationTest { + + @BeforeAll + static void prepare() { + setUpTenant(CENTRAL_TENANT_ID); + setUpTenant(MEMBER_TENANT_ID); + saveLocationRecords(); + } + + @AfterAll + static void cleanUp() { + removeTenant(MEMBER_TENANT_ID); + removeTenant(CENTRAL_TENANT_ID); + } + + @Test + void doGetConsortiumLocations_returns200AndRecords() { + List> queryParams = List.of(); + + var result = doGet(consortiumLocationsSearchPath(queryParams), CENTRAL_TENANT_ID); + var actual = parseResponse(result, ConsortiumLocationCollection.class); + + assertThat(actual.getLocations()).hasSize(7); + assertThat(actual.getTotalRecords()).isEqualTo(7); + assertThat(actual.getLocations()) + .allSatisfy(location -> { + assertThat(location.getId()).isNotBlank(); + assertThat(location.getName()).isNotBlank(); + assertThat(location.getTenantId()).isNotBlank(); + }); + } + + @Test + void doGetConsortiumLocations_returns200AndRecords_withAllQueryParams() { + List> queryParams = List.of( + pair("tenantId", "consortium"), + pair("limit", "5"), + pair("offset", "0"), + pair("sortBy", "name"), + pair("sortOrder", "asc") + ); + + var result = doGet(consortiumLocationsSearchPath(queryParams), CENTRAL_TENANT_ID); + var actual = parseResponse(result, ConsortiumLocationCollection.class); + + assertThat(actual.getLocations()).hasSize(5); + assertThat(actual.getTotalRecords()).isEqualTo(7); + assertThat(actual.getLocations().get(0).getTenantId()).isEqualTo(CENTRAL_TENANT_ID); + //check sortBy name + assertThat(actual.getLocations().get(0).getName()).isEqualTo("Annex"); + assertThat(actual.getLocations().get(1).getName()).isEqualTo("DCB"); + } + + private static void saveLocationRecords() { + getLocationsSampleAsMap().stream().map( + location -> kafkaResourceEvent(CENTRAL_TENANT_ID, CREATE, location, null)) + .forEach(event -> kafkaTemplate.send(inventoryLocationTopic(CENTRAL_TENANT_ID), event)); + await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { + var totalHits = countIndexDocument(LOCATION_RESOURCE, CENTRAL_TENANT_ID); + assertThat(totalHits).isEqualTo(7); + }); + } +} diff --git a/src/test/java/org/folio/search/controller/LocationsIndexingConsortiumIT.java b/src/test/java/org/folio/search/controller/LocationsIndexingConsortiumIT.java index 3aab0131e..a7761acab 100644 --- a/src/test/java/org/folio/search/controller/LocationsIndexingConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/LocationsIndexingConsortiumIT.java @@ -93,7 +93,7 @@ void shouldRemoveAllDocumentsByTenantIdOnDeleteAllEvent() { awaitAssertLocationCount(1); } - private void awaitAssertLocationCount(int expected) { + public static void awaitAssertLocationCount(int expected) { await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { var totalHits = countIndexDocument(LOCATION_RESOURCE, CENTRAL_TENANT_ID); assertThat(totalHits).isEqualTo(expected); diff --git a/src/test/java/org/folio/search/repository/ConsortiumLocationRepositoryTest.java b/src/test/java/org/folio/search/repository/ConsortiumLocationRepositoryTest.java new file mode 100644 index 000000000..ef292e3e7 --- /dev/null +++ b/src/test/java/org/folio/search/repository/ConsortiumLocationRepositoryTest.java @@ -0,0 +1,153 @@ +package org.folio.search.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.repository.ConsortiumLocationRepository.LOCATION_INDEX; +import static org.folio.search.utils.TestConstants.INDEX_NAME; +import static org.folio.search.utils.TestConstants.MEMBER_TENANT_ID; +import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.opensearch.client.RequestOptions.DEFAULT; + +import java.io.IOException; +import org.folio.search.domain.dto.ConsortiumLocation; +import org.folio.search.model.SearchResult; +import org.folio.search.service.converter.ElasticsearchDocumentConverter; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.search.sort.FieldSortBuilder; +import org.opensearch.search.sort.SortOrder; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class ConsortiumLocationRepositoryTest { + + @Mock + private IndexNameProvider indexNameProvider; + @Mock + private ElasticsearchDocumentConverter documentConverter; + @Mock + private RestHighLevelClient client; + @InjectMocks + private ConsortiumLocationRepository repository; + + @Captor + private ArgumentCaptor requestCaptor; + + @BeforeEach + void setUp() { + lenient().when(indexNameProvider.getIndexName(LOCATION_INDEX, TENANT_ID)).thenReturn(INDEX_NAME); + } + + @Test + void fetchLocations_positive() throws IOException { + var limit = 123; + var offset = 321; + var sortBy = "test"; + var searchResponse = mock(SearchResponse.class); + var searchResult = Mockito.>mock(); + + when(client.search(requestCaptor.capture(), eq(DEFAULT))).thenReturn(searchResponse); + when(documentConverter.convertToSearchResult(searchResponse, ConsortiumLocation.class)).thenReturn(searchResult); + + var actual = repository.fetchLocations(TENANT_ID, null, limit, offset, sortBy, null); + + assertThat(actual).isEqualTo(searchResult); + assertThat(requestCaptor.getValue()) + .matches(request -> request.indices().length == 1 && request.indices()[0].equals(INDEX_NAME)) + .satisfies(request -> { + var source = request.source(); + assertThat(source.size()).isEqualTo(limit); + assertThat(source.from()).isEqualTo(offset); + + assertThat(source.sorts()).hasSize(1); + assertThat(source.sorts().get(0)).isInstanceOf(FieldSortBuilder.class); + var sort = (FieldSortBuilder) source.sorts().get(0); + assertThat(sort.getFieldName()).isEqualTo(sortBy); + assertThat(sort.order()).isEqualTo(SortOrder.ASC); + + assertThat(source.query()).isNull(); + }); + } + + @Test + void fetchLocations_positive_withTenantFilter() throws IOException { + var limit = 123; + var offset = 321; + var sortBy = "test"; + var searchResponse = mock(SearchResponse.class); + var searchResult = Mockito.>mock(); + + when(client.search(requestCaptor.capture(), eq(DEFAULT))).thenReturn(searchResponse); + when(documentConverter.convertToSearchResult(searchResponse, ConsortiumLocation.class)).thenReturn(searchResult); + + var actual = repository.fetchLocations(TENANT_ID, MEMBER_TENANT_ID, limit, offset, sortBy, null); + + assertThat(actual).isEqualTo(searchResult); + assertThat(requestCaptor.getValue()) + .matches(request -> request.indices().length == 1 && request.indices()[0].equals(INDEX_NAME)) + .satisfies(request -> { + var source = request.source(); + assertThat(source.size()).isEqualTo(limit); + assertThat(source.from()).isEqualTo(offset); + + assertThat(source.sorts()).hasSize(1); + assertThat(source.sorts().get(0)).isInstanceOf(FieldSortBuilder.class); + var sort = (FieldSortBuilder) source.sorts().get(0); + assertThat(sort.getFieldName()).isEqualTo(sortBy); + assertThat(sort.order()).isEqualTo(SortOrder.ASC); + + assertThat(source.query()).isInstanceOf(TermQueryBuilder.class); + var query = (TermQueryBuilder) source.query(); + assertThat(query.fieldName()).isEqualTo("tenantId"); + assertThat(query.value()).isEqualTo(MEMBER_TENANT_ID); + }); + } + + @Test + void fetchLocations_positive_sortDesc() throws IOException { + var limit = 123; + var offset = 321; + var sortBy = "test"; + var searchResponse = mock(SearchResponse.class); + var searchResult = Mockito.>mock(); + + when(client.search(requestCaptor.capture(), eq(DEFAULT))).thenReturn(searchResponse); + when(documentConverter.convertToSearchResult(searchResponse, ConsortiumLocation.class)).thenReturn(searchResult); + + var actual = repository.fetchLocations(TENANT_ID, null, limit, offset, sortBy, + org.folio.search.domain.dto.SortOrder.DESC); + + assertThat(actual).isEqualTo(searchResult); + assertThat(requestCaptor.getValue()) + .matches(request -> request.indices().length == 1 && request.indices()[0].equals(INDEX_NAME)) + .satisfies(request -> { + var source = request.source(); + assertThat(source.size()).isEqualTo(limit); + assertThat(source.from()).isEqualTo(offset); + + assertThat(source.sorts()).hasSize(1); + assertThat(source.sorts().get(0)).isInstanceOf(FieldSortBuilder.class); + var sort = (FieldSortBuilder) source.sorts().get(0); + assertThat(sort.getFieldName()).isEqualTo(sortBy); + assertThat(sort.order()).isEqualTo(SortOrder.DESC); + + assertThat(source.query()).isNull(); + }); + } + +} diff --git a/src/test/java/org/folio/search/sample/SampleLocations.java b/src/test/java/org/folio/search/sample/SampleLocations.java new file mode 100644 index 000000000..2da0d1b23 --- /dev/null +++ b/src/test/java/org/folio/search/sample/SampleLocations.java @@ -0,0 +1,21 @@ +package org.folio.search.sample; + +import static org.folio.search.utils.TestUtils.readJsonFromFile; + +import com.fasterxml.jackson.core.type.TypeReference; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SampleLocations { + + private static final List> LOCATIONS_RECORD_AS_MAP = + readJsonFromFile("/samples/location-sample/locations.json", new TypeReference<>() { }); + + public static List> getLocationsSampleAsMap() { + return LOCATIONS_RECORD_AS_MAP; + } +} diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumLocationServiceTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumLocationServiceTest.java new file mode 100644 index 000000000..5afc63b32 --- /dev/null +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumLocationServiceTest.java @@ -0,0 +1,68 @@ +package org.folio.search.service.consortium; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.folio.search.domain.dto.ConsortiumLocation; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.SearchResult; +import org.folio.search.repository.ConsortiumLocationRepository; +import org.folio.spring.testing.type.UnitTest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +public class ConsortiumLocationServiceTest { + + public static final String ID = "id"; + public static final String LOCATION_NAME = "location name"; + public static final String CONSORTIUM_TENANT = "consortium"; + public static final String NAME = "name"; + @Mock + private ConsortiumLocationRepository repository; + + @InjectMocks + private ConsortiumLocationService service; + + @Test + void fetchLocations_ValidSortBy() { + var tenantHeader = CONSORTIUM_TENANT; + var tenantId = CONSORTIUM_TENANT; + var sortOrder = SortOrder.ASC; + var sortBy = NAME; + var limit = 10; + var offset = 0; + var searchResult = prepareSearchResult(); + + when(repository.fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder)) + .thenReturn(searchResult); + + var actual = service.fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + + assertThat(actual).isNotNull(); + assertThat(actual.getRecords()).hasSize(1); + assertThat(actual.getRecords().get(0).getTenantId()).isEqualTo(CONSORTIUM_TENANT); + assertThat(actual.getRecords().get(0).getName()).isEqualTo(LOCATION_NAME); + assertThat(actual.getRecords().get(0).getId()).isEqualTo(ID); + verify(repository).fetchLocations(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + } + + @NotNull + private static SearchResult prepareSearchResult() { + var location = new ConsortiumLocation(); + location.setId(ID); + location.setName(LOCATION_NAME); + location.setTenantId(CONSORTIUM_TENANT); + + var searchResult = new SearchResult(); + searchResult.records(List.of(location)); + return searchResult; + } +} diff --git a/src/test/java/org/folio/search/support/base/ApiEndpoints.java b/src/test/java/org/folio/search/support/base/ApiEndpoints.java index b3206ed62..1309e2b69 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -25,6 +25,14 @@ public static String consortiumHoldingsSearchPath(List> que return addQueryParams(consortiumHoldingsSearchPath(), queryParams); } + public static String consortiumLocationsSearchPath() { + return "/search/consortium/locations"; + } + + public static String consortiumLocationsSearchPath(List> queryParams) { + return addQueryParams(consortiumLocationsSearchPath(), queryParams); + } + public static String consortiumItemsSearchPath() { return "/search/consortium/items"; } diff --git a/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java b/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java index bea051912..980c0ddca 100644 --- a/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java +++ b/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java @@ -112,5 +112,4 @@ protected static ResultActions doSearchByInstances(String query) { protected static ResultActions doSearchByInstances(String query, boolean expandAll) { return doSearch(instanceSearchPath(), MEMBER_TENANT_ID, query, null, null, expandAll); } - } diff --git a/src/test/resources/samples/location-sample/locations.json b/src/test/resources/samples/location-sample/locations.json new file mode 100644 index 000000000..57296a7a7 --- /dev/null +++ b/src/test/resources/samples/location-sample/locations.json @@ -0,0 +1,129 @@ +[ + { + "id": "53cf956f-c1df-410b-8bea-27f712cca7c0", + "name": "Annex", + "code": "KU/CC/DI/A", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "libraryId": "5d78803e-ca04-4b4a-aeae-2c63b924518b", + "primaryServicePoint": "3a40852d-49fd-4df2-a1f9-6e2641a6e91f", + "servicePointIds": [ + "3a40852d-49fd-4df2-a1f9-6e2641a6e91f" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.283+00:00", + "updatedDate": "2024-04-15T13:49:06.283+00:00" + } + }, + { + "id": "184aae84-a5bf-4c6a-85ba-4a7c73026cd5", + "name": "Online", + "code": "E", + "description": "Use for online resources", + "discoveryDisplayName": "Online", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "libraryId": "c2549bb4-19c7-4fcc-8b52-39e612fb7dbe", + "primaryServicePoint": "bba36e5d-d567-45fa-81cd-b25874472e30", + "servicePointIds": [ + "bba36e5d-d567-45fa-81cd-b25874472e30" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.272+00:00", + "updatedDate": "2024-04-15T13:49:06.272+00:00" + } + }, + { + "id": "9d1b77e8-f02e-4b7f-b296-3f2042ddac54", + "name": "DCB", + "code": "000", + "institutionId": "9d1b77e5-f02e-4b7f-b296-3f2042ddac54", + "campusId": "9d1b77e6-f02e-4b7f-b296-3f2042ddac54", + "libraryId": "9d1b77e7-f02e-4b7f-b296-3f2042ddac54", + "primaryServicePoint": "9d1b77e8-f02e-4b7f-b296-3f2042ddac54", + "servicePointIds": [ + "9d1b77e8-f02e-4b7f-b296-3f2042ddac54" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:51:08.526+00:00", + "updatedDate": "2024-04-15T13:51:08.526+00:00" + } + }, + { + "id": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "name": "Main Library", + "code": "KU/CC/DI/M", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "libraryId": "5d78803e-ca04-4b4a-aeae-2c63b924518b", + "primaryServicePoint": "3a40852d-49fd-4df2-a1f9-6e2641a6e91f", + "servicePointIds": [ + "3a40852d-49fd-4df2-a1f9-6e2641a6e91f" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.265+00:00", + "updatedDate": "2024-04-15T13:49:06.265+00:00" + } + }, + { + "id": "758258bc-ecc1-41b8-abca-f7b610822ffd", + "name": "ORWIG ETHNO CD", + "code": "KU/CC/DI/O", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "libraryId": "5d78803e-ca04-4b4a-aeae-2c63b924518b", + "primaryServicePoint": "3a40852d-49fd-4df2-a1f9-6e2641a6e91f", + "servicePointIds": [ + "3a40852d-49fd-4df2-a1f9-6e2641a6e91f" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.271+00:00", + "updatedDate": "2024-04-15T13:49:06.271+00:00" + } + }, + { + "id": "b241764c-1466-4e1d-a028-1a3684a5da87", + "name": "Popular Reading Collection", + "code": "KU/CC/DI/P", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "libraryId": "5d78803e-ca04-4b4a-aeae-2c63b924518b", + "primaryServicePoint": "3a40852d-49fd-4df2-a1f9-6e2641a6e91f", + "servicePointIds": [ + "3a40852d-49fd-4df2-a1f9-6e2641a6e91f" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.263+00:00", + "updatedDate": "2024-04-15T13:49:06.263+00:00" + } + }, + { + "id": "f34d27c6-a8eb-461b-acd6-5dea81771e70", + "name": "SECOND FLOOR", + "code": "KU/CC/DI/2", + "isActive": true, + "institutionId": "40ee00ca-a518-4b49-be01-0638d0a4ac57", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "libraryId": "5d78803e-ca04-4b4a-aeae-2c63b924518b", + "primaryServicePoint": "3a40852d-49fd-4df2-a1f9-6e2641a6e91f", + "servicePointIds": [ + "3a40852d-49fd-4df2-a1f9-6e2641a6e91f" + ], + "servicePoints": [], + "metadata": { + "createdDate": "2024-04-15T13:49:06.279+00:00", + "updatedDate": "2024-04-15T13:49:06.279+00:00" + } + } +]