diff --git a/NEWS.md b/NEWS.md index b167fe04a..f8bcfba48 100644 --- a/NEWS.md +++ b/NEWS.md @@ -24,6 +24,7 @@ * Extend response with additional Location fields for Inventory Locations in a Consortium endpoint ([MSEARCH-775](https://folio-org.atlassian.net/browse/MSEARCH-775)) * Implement Indexing of Institutions from Kafka ([MSEARCH-771](https://issues.folio.org/browse/MSEARCH-771)) * Implement Indexing of Libraries from Kafka ([MSEARCH-769](https://issues.folio.org/browse/MSEARCH-769)) +* Return Unified List of Inventory Campuses in a Consortium ([MSEARCH-773](https://issues.folio.org/browse/MSEARCH-773)) ### 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 adde835c5..51abd3833 100644 --- a/README.md +++ b/README.md @@ -869,6 +869,7 @@ Special API that provide consolidated access to records in consortium environmen | GET | `/search/consortium/holdings` | Returns consolidated holdings | | GET | `/search/consortium/items` | Returns consolidated items | | GET | `/search/consortium/locations` | Returns consolidated locations | +| GET | `/search/consortium/campuses` | Returns consolidated campuses | ## Additional Information diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index e741c054a..acc4ca3e3 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -189,6 +189,18 @@ "user-tenants.collection.get" ] }, + { + "methods": [ + "GET" + ], + "pathPattern": "/search/consortium/campuses", + "permissionsRequired": [ + "consortium-search.campuses.collection.get" + ], + "modulePermissions": [ + "user-tenants.collection.get" + ] + }, { "methods": [ "GET" @@ -724,6 +736,11 @@ "permissionName": "consortium-search.locations.collection.get", "displayName": "Consortium Search - fetch locations records", "description": "Returns location records in consortium" + }, + { + "permissionName": "consortium-search.campuses.collection.get", + "displayName": "Consortium Search - fetch campuses records", + "description": "Returns campus 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 aa987a5ba..9e4c42739 100644 --- a/src/main/java/org/folio/search/controller/SearchConsortiumController.java +++ b/src/main/java/org/folio/search/controller/SearchConsortiumController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.search.domain.dto.BatchIdsDto; +import org.folio.search.domain.dto.ConsortiumCampusCollection; import org.folio.search.domain.dto.ConsortiumHolding; import org.folio.search.domain.dto.ConsortiumHoldingCollection; import org.folio.search.domain.dto.ConsortiumItem; @@ -21,6 +22,7 @@ import org.folio.search.model.service.CqlSearchRequest; import org.folio.search.model.types.ResourceType; import org.folio.search.rest.resource.SearchConsortiumApi; +import org.folio.search.service.consortium.ConsortiumCampusService; import org.folio.search.service.consortium.ConsortiumInstanceSearchService; import org.folio.search.service.consortium.ConsortiumInstanceService; import org.folio.search.service.consortium.ConsortiumLocationService; @@ -45,6 +47,7 @@ public class SearchConsortiumController implements SearchConsortiumApi { private final ConsortiumInstanceService instanceService; private final ConsortiumLocationService locationService; private final ConsortiumInstanceSearchService searchService; + private final ConsortiumCampusService campusService; @Override public ResponseEntity getConsortiumHoldings(String tenantHeader, String instanceId, @@ -96,6 +99,21 @@ public ResponseEntity getConsortiumLocations(Strin .totalRecords(result.getTotalRecords())); } + @Override + public ResponseEntity getConsortiumCampuses(String tenantHeader, + String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + var result = campusService.fetchCampuses(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + + return ResponseEntity.ok(new + ConsortiumCampusCollection() + .campuses(result.getRecords()) + .totalRecords(result.getTotalRecords())); + } + @Override public ResponseEntity getConsortiumHolding(UUID id, String tenantHeader) { var tenant = verifyAndGetTenant(tenantHeader); diff --git a/src/main/java/org/folio/search/repository/ConsortiumCampusRepository.java b/src/main/java/org/folio/search/repository/ConsortiumCampusRepository.java new file mode 100644 index 000000000..b1c07733e --- /dev/null +++ b/src/main/java/org/folio/search/repository/ConsortiumCampusRepository.java @@ -0,0 +1,79 @@ +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.ConsortiumCampus; +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 ConsortiumCampusRepository { + + public static final String CAMPUS_INDEX = "campus"; + private static final String OPERATION_TYPE = "searchApi"; + private final IndexNameProvider indexNameProvider; + private final ElasticsearchDocumentConverter documentConverter; + + private final RestHighLevelClient client; + + public SearchResult fetchCampuses(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, ConsortiumCampus.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(CAMPUS_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/ConsortiumCampusService.java b/src/main/java/org/folio/search/service/consortium/ConsortiumCampusService.java new file mode 100644 index 000000000..3fa1a6151 --- /dev/null +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumCampusService.java @@ -0,0 +1,45 @@ +package org.folio.search.service.consortium; + +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.search.domain.dto.ConsortiumCampus; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.SearchResult; +import org.folio.search.repository.ConsortiumCampusRepository; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ConsortiumCampusService { + + private static final Set VALID_SORT_BY = Set.of("id", "name", "tenantId"); + private final ConsortiumCampusRepository repository; + private final ConsortiumTenantExecutor executor; + + public SearchResult fetchCampuses(String tenantHeader, + String tenantId, + Integer limit, + Integer offset, + String sortBy, + SortOrder sortOrder) { + log.info("fetching consortium campuses for tenant: {}, tenantId: {}, sortBy: {}", + tenantHeader, + tenantId, + sortBy); + + validateSortByValue(sortBy); + + return executor.execute( + tenantHeader, + () -> repository.fetchCampuses(tenantHeader, tenantId, limit, offset, sortBy, sortOrder)); + } + + private void validateSortByValue(String sortBy) { + if (!VALID_SORT_BY.contains(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 71390f7e3..7b626d59c 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -76,6 +76,9 @@ paths: /search/consortium/locations: $ref: 'paths/search-consortium/search-consortium-locations.yaml' + /search/consortium/campuses: + $ref: 'paths/search-consortium/search-consortium-campuses.yaml' + /search/consortium/batch/items: $ref: 'paths/search-consortium/search-consortium-batch-items.yaml' diff --git a/src/main/resources/swagger.api/parameters/consortium-campuses-limit-param.yaml b/src/main/resources/swagger.api/parameters/consortium-campuses-limit-param.yaml new file mode 100644 index 000000000..aa4edf38b --- /dev/null +++ b/src/main/resources/swagger.api/parameters/consortium-campuses-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-campus-name-param.yaml b/src/main/resources/swagger.api/parameters/sort-by-campus-name-param.yaml new file mode 100644 index 000000000..3504847bb --- /dev/null +++ b/src/main/resources/swagger.api/parameters/sort-by-campus-name-param.yaml @@ -0,0 +1,13 @@ +in: query +name: sortBy +description: | + Defines a field to sort by. + Possible values: + - id + - tenantId + - institutionId + - name +required: false +schema: + type: string + default: name diff --git a/src/main/resources/swagger.api/paths/search-consortium/search-consortium-campuses.yaml b/src/main/resources/swagger.api/paths/search-consortium/search-consortium-campuses.yaml new file mode 100644 index 000000000..1990bc832 --- /dev/null +++ b/src/main/resources/swagger.api/paths/search-consortium/search-consortium-campuses.yaml @@ -0,0 +1,24 @@ +get: + operationId: getConsortiumCampuses + summary: Get Consortium Campuses + description: Get a list of campuses (only for consortium environment) + tags: + - search-consortium + parameters: + - $ref: '../../parameters/tenant-id-query-param.yaml' + - $ref: '../../parameters/consortium-campuses-limit-param.yaml' + - $ref: '../../parameters/offset-param.yaml' + - $ref: '../../parameters/sort-by-campus-name-param.yaml' + - $ref: '../../parameters/sort-order-param.yaml' + - $ref: '../../parameters/x-okapi-tenant-header.yaml' + responses: + '200': + description: List of campuses + content: + application/json: + schema: + $ref: '../../schemas/entity/consortiumCampusCollection.yaml' + '400': + $ref: '../../responses/badRequestResponse.yaml' + '500': + $ref: '../../responses/internalServerErrorResponse.yaml' diff --git a/src/main/resources/swagger.api/schemas/entity/consortiumCampus.yaml b/src/main/resources/swagger.api/schemas/entity/consortiumCampus.yaml new file mode 100644 index 000000000..26103e4fe --- /dev/null +++ b/src/main/resources/swagger.api/schemas/entity/consortiumCampus.yaml @@ -0,0 +1,16 @@ +type: object +properties: + id: + description: Campus ID + type: string + tenantId: + description: Tenant ID of the Campus + type: string + name: + description: Campus name + type: string + institutionId: + description: The UUID of the institution, the first-level location unit, this (shelf) campus belongs to. + type: string + metadata: + $ref: "../dto/common/metadata.yaml" diff --git a/src/main/resources/swagger.api/schemas/entity/consortiumCampusCollection.yaml b/src/main/resources/swagger.api/schemas/entity/consortiumCampusCollection.yaml new file mode 100644 index 000000000..d9d10505a --- /dev/null +++ b/src/main/resources/swagger.api/schemas/entity/consortiumCampusCollection.yaml @@ -0,0 +1,8 @@ +type: object +properties: + campuses: + type: array + items: + $ref: './consortiumCampus.yaml' + totalRecords: + type: integer diff --git a/src/test/java/org/folio/search/controller/ConsortiumSearchCampusesIT.java b/src/test/java/org/folio/search/controller/ConsortiumSearchCampusesIT.java new file mode 100644 index 000000000..f291af9d4 --- /dev/null +++ b/src/test/java/org/folio/search/controller/ConsortiumSearchCampusesIT.java @@ -0,0 +1,115 @@ +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.SampleCampuses.getCampusesSampleAsMap; +import static org.folio.search.support.base.ApiEndpoints.consortiumCampusesSearchPath; +import static org.folio.search.utils.SearchUtils.CAMPUS_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.inventoryCampusTopic; +import static org.folio.search.utils.TestUtils.kafkaResourceEvent; +import static org.folio.search.utils.TestUtils.parseResponse; + +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.groups.Tuple; +import org.folio.search.domain.dto.ConsortiumCampus; +import org.folio.search.domain.dto.ConsortiumCampusCollection; +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; +import org.junit.platform.commons.util.StringUtils; + +@IntegrationTest +class ConsortiumSearchCampusesIT extends BaseConsortiumIntegrationTest { + + private static final int EXPECTED_WITH_TWO_TENANTS = 18; + private static final int EXPECTED_WITH_SINGLE_TENANT = 9; + + @BeforeAll + static void prepare() { + setUpTenant(CENTRAL_TENANT_ID); + setUpTenant(MEMBER_TENANT_ID); + saveCampusRecords(); + } + + @AfterAll + static void cleanUp() { + removeTenant(MEMBER_TENANT_ID); + removeTenant(CENTRAL_TENANT_ID); + } + + @Test + void doGetConsortiumCampuses_returns200AndRecords() { + List> queryParams = List.of(); + + var result = doGet(consortiumCampusesSearchPath(queryParams), MEMBER_TENANT_ID); + var actual = parseResponse(result, ConsortiumCampusCollection.class); + + assertThat(actual.getCampuses()).hasSize(EXPECTED_WITH_TWO_TENANTS); + + assertThat(actual.getTotalRecords()).isEqualTo(EXPECTED_WITH_TWO_TENANTS); + + assertThat(actual.getCampuses()) + .filteredOn(location -> location.getTenantId().equals(MEMBER_TENANT_ID)) + .hasSize(EXPECTED_WITH_SINGLE_TENANT); + + assertThat(actual.getCampuses()) + .filteredOn(location -> location.getTenantId().equals(CENTRAL_TENANT_ID)) + .hasSize(EXPECTED_WITH_SINGLE_TENANT); + + assertThat(actual.getCampuses()) + .extracting(ConsortiumCampus::getId, ConsortiumCampus::getName, ConsortiumCampus::getTenantId, + ConsortiumCampus::getInstitutionId) + .map(Tuple::toList) + .matches(locations -> locations.stream().allMatch(obj -> StringUtils.isNotBlank(obj.toString()))); + + assertThat(actual.getCampuses()) + .map(ConsortiumCampus::getMetadata) + .filteredOn(metadata -> metadata.getCreatedDate() != null && metadata.getUpdatedDate() != null) + .hasSize(EXPECTED_WITH_TWO_TENANTS); + } + + @Test + void doGetConsortiumCampuses_returns200AndRecords_withAllQueryParams() { + List> queryParams = List.of( + pair("tenantId", "consortium"), + pair("limit", "5"), + pair("offset", "0"), + pair("sortBy", "name"), + pair("sortOrder", "asc") + ); + + var result = doGet(consortiumCampusesSearchPath(queryParams), CENTRAL_TENANT_ID); + var actual = parseResponse(result, ConsortiumCampusCollection.class); + + assertThat(actual.getCampuses()).hasSize(5); + assertThat(actual.getTotalRecords()).isEqualTo(EXPECTED_WITH_SINGLE_TENANT); + assertThat(actual.getCampuses().get(0).getTenantId()).isEqualTo(CENTRAL_TENANT_ID); + // check sortBy name + assertThat(actual.getCampuses().get(0).getName()).isEqualTo("My campus 1"); + assertThat(actual.getCampuses().get(1).getName()).isEqualTo("My campus 2"); + } + + private static void saveCampusRecords() { + getCampusesSampleAsMap().stream() + .flatMap(campus -> Stream.of( + kafkaResourceEvent(CENTRAL_TENANT_ID, CREATE, campus, null), + kafkaResourceEvent(MEMBER_TENANT_ID, CREATE, campus, null))) + .forEach(event -> kafkaTemplate.send(inventoryCampusTopic(event.getTenant()), event)); + + await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { + var totalHits = countIndexDocument(CAMPUS_RESOURCE, CENTRAL_TENANT_ID); + + assertThat(totalHits).isEqualTo(EXPECTED_WITH_TWO_TENANTS); + }); + } +} diff --git a/src/test/java/org/folio/search/repository/ConsortiumCampusRepositoryTest.java b/src/test/java/org/folio/search/repository/ConsortiumCampusRepositoryTest.java new file mode 100644 index 000000000..6f661d472 --- /dev/null +++ b/src/test/java/org/folio/search/repository/ConsortiumCampusRepositoryTest.java @@ -0,0 +1,155 @@ +package org.folio.search.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.repository.ConsortiumCampusRepository.CAMPUS_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.ConsortiumCampus; +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 ConsortiumCampusRepositoryTest { + + @Mock + private IndexNameProvider indexNameProvider; + @Mock + private ElasticsearchDocumentConverter documentConverter; + @Mock + private RestHighLevelClient client; + @InjectMocks + private ConsortiumCampusRepository repository; + + @Captor + private ArgumentCaptor requestCaptor; + + @BeforeEach + void setUp() { + lenient().when(indexNameProvider.getIndexName(CAMPUS_INDEX, TENANT_ID)).thenReturn(INDEX_NAME); + } + + @Test + void fetchCampuses_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, ConsortiumCampus.class)).thenReturn(searchResult); + + var actual = repository.fetchCampuses(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 fetchCampuses_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, ConsortiumCampus.class)).thenReturn(searchResult); + + var actual = repository.fetchCampuses(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 fetchCampuses_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, ConsortiumCampus.class)).thenReturn(searchResult); + + var actual = repository.fetchCampuses(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/SampleCampuses.java b/src/test/java/org/folio/search/sample/SampleCampuses.java new file mode 100644 index 000000000..b7b1fa15d --- /dev/null +++ b/src/test/java/org/folio/search/sample/SampleCampuses.java @@ -0,0 +1,20 @@ +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 SampleCampuses { + + private static final List> CAMPUSES_RECORD_AS_MAP = + readJsonFromFile("/samples/campus-sample/campuses.json", new TypeReference<>() { }); + + public static List> getCampusesSampleAsMap() { + return CAMPUSES_RECORD_AS_MAP; + } +} diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumCampusServiceTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumCampusServiceTest.java new file mode 100644 index 000000000..259ca95ef --- /dev/null +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumCampusServiceTest.java @@ -0,0 +1,94 @@ +package org.folio.search.service.consortium; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; +import org.folio.search.domain.dto.ConsortiumCampus; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.SearchResult; +import org.folio.search.repository.ConsortiumCampusRepository; +import org.folio.spring.testing.type.UnitTest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +public class ConsortiumCampusServiceTest { + + public static final String ID = UUID.randomUUID().toString(); + public static final String CAMPUS_NAME = "campus name"; + public static final String CONSORTIUM_TENANT = "consortium"; + + public static final String SORT_BY_ID = "id"; + public static final String SORT_BT_NAME = "name"; + public static final String SORT_BY_TENANT_ID = "tenantId"; + + @Mock + private ConsortiumCampusRepository repository; + @Mock + private ConsortiumTenantExecutor executor; + + @InjectMocks + private ConsortiumCampusService service; + + @ParameterizedTest + @SuppressWarnings("unchecked") + @ValueSource(strings = {SORT_BY_ID, SORT_BT_NAME, SORT_BY_TENANT_ID}) + void fetchCampuses_ValidSortBy(String sortBy) { + var tenantHeader = CONSORTIUM_TENANT; + var tenantId = CONSORTIUM_TENANT; + var sortOrder = SortOrder.ASC; + var limit = 10; + var offset = 0; + var searchResult = prepareSearchResult(); + + when(repository.fetchCampuses(tenantHeader, tenantId, limit, offset, sortBy, sortOrder)) + .thenReturn(searchResult); + when(executor.execute(eq(tenantId), any(Supplier.class))) + .thenAnswer(invocation -> ((Supplier) invocation.getArgument(1)).get()); + + var actual = service.fetchCampuses(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + + assertThat(actual).isEqualTo(searchResult); + verify(repository).fetchCampuses(tenantHeader, tenantId, limit, offset, sortBy, sortOrder); + verify(executor).execute(eq(tenantId), any(Supplier.class)); + } + + @Test + void fetchCampuses_InvalidSortBy() { + var sortOrder = SortOrder.ASC; + var limit = 10; + var offset = 0; + + Assertions.assertThrows(IllegalArgumentException.class, () -> + service.fetchCampuses(CONSORTIUM_TENANT, CONSORTIUM_TENANT, limit, offset, "invalid", sortOrder) + ); + } + + @NotNull + private static SearchResult prepareSearchResult() { + var campus = new ConsortiumCampus() + .id(ID) + .name(CAMPUS_NAME) + .tenantId(CONSORTIUM_TENANT) + .institutionId(ID); + + var searchResult = new SearchResult(); + searchResult.records(List.of(campus)); + + 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 5f8c14f3d..e2348b2cf 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -33,6 +33,14 @@ public static String consortiumLocationsSearchPath(List> qu return addQueryParams(consortiumLocationsSearchPath(), queryParams); } + public static String consortiumCampusesSearchPath() { + return "/search/consortium/campuses"; + } + + public static String consortiumCampusesSearchPath(List> queryParams) { + return addQueryParams(consortiumCampusesSearchPath(), queryParams); + } + public static String consortiumItemsSearchPath() { return "/search/consortium/items"; } diff --git a/src/test/resources/samples/campus-sample/campuses.json b/src/test/resources/samples/campus-sample/campuses.json new file mode 100644 index 000000000..7cbc9edc3 --- /dev/null +++ b/src/test/resources/samples/campus-sample/campuses.json @@ -0,0 +1,92 @@ +[ + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe21", + "name": "My campus 1", + "code": "MC1", + "campusId": "62cf76b7-cca5-4d33-9217-edf42ce1a848", + "metadata": { + "createdDate": "2024-06-25T05:28:41.306+00:00", + "updatedDate": "2024-06-25T05:28:41.306+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe22", + "name": "My campus 2", + "code": "MC2", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe23", + "name": "My campus 3", + "code": "MC3", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe24", + "name": "My campus 4", + "code": "MC4", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe25", + "name": "My campus 5", + "code": "MC5", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe26", + "name": "My campus 6", + "code": "MC6", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe27", + "name": "My campus 7", + "code": "MC7", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe28", + "name": "My campus 8", + "code": "MC8", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + }, + { + "id": "83891666-dcb6-4cd7-ad3a-f4b305abfe29", + "name": "My campus 9", + "code": "MC9", + "campusId": "470ff1dd-937a-4195-bf9e-06bcfcd135df", + "metadata": { + "createdDate": "2024-06-25T05:28:41.312+00:00", + "updatedDate": "2024-06-25T05:28:41.312+00:00" + } + } +]