From 7f9ffa4200b48e6eccca00678544217d0070b6f8 Mon Sep 17 00:00:00 2001 From: psmagin Date: Mon, 11 Mar 2024 16:14:16 +0200 Subject: [PATCH] feat(search-instances): implement endpoint for consolidate holdings access in consortium Closes: MSEARCH-692 Signed-off-by: psmagin --- .../folio/search/configuration/WebConfig.java | 9 + .../SearchConsortiumController.java | 54 +++ .../service/ConsortiumSearchContext.java | 100 +++++ .../search/model/types/ResourceType.java | 2 +- .../ConsortiumInstanceRepository.java | 21 +- .../consortium/ConsortiumInstanceService.java | 8 + .../ConsortiumSearchQueryBuilder.java | 125 ++++++ .../resources/swagger.api/mod-search.yaml | 117 ++++++ .../swagger.api/schemas/holding.json | 4 + .../BrowseClassificationConsortiumIT.java | 10 +- .../BrowseContributorConsortiumIT.java | 14 +- .../controller/BrowseSubjectConsortiumIT.java | 12 +- .../SearchHoldingsConsortiumIT.java | 109 +++++ .../SearchInstanceConsortiumIT.java | 325 +-------------- .../search/controller/SearchInstanceIT.java | 92 +---- .../service/ConsortiumSearchContextTest.java | 52 +++ .../sample/SampleInstancesResponse.java | 31 ++ .../search/service/IndexServiceTest.java | 4 +- .../service/SearchTenantServiceTest.java | 4 +- .../consortium/ConsortiaServiceTest.java | 6 +- .../ConsortiumSearchHelperTest.java | 34 +- .../ConsortiumSearchQueryBuilderTest.java | 206 ++++++++++ .../ConsortiumTenantExecutorTest.java | 14 +- .../ConsortiumTenantProviderTest.java | 6 +- ...horitySearchResponsePostProcessorTest.java | 22 +- .../search/support/base/ApiEndpoints.java | 15 + .../base/BaseConsortiumIntegrationTest.java | 38 +- .../org/folio/search/utils/TestConstants.java | 2 +- .../instance-basic-response.json | 67 +++ .../instance-full-response.json | 383 ++++++++++++++++++ .../samples/semantic-web-primer/holdings.json | 1 + 31 files changed, 1423 insertions(+), 464 deletions(-) create mode 100644 src/main/java/org/folio/search/controller/SearchConsortiumController.java create mode 100644 src/main/java/org/folio/search/model/service/ConsortiumSearchContext.java create mode 100644 src/main/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilder.java create mode 100644 src/test/java/org/folio/search/controller/SearchHoldingsConsortiumIT.java create mode 100644 src/test/java/org/folio/search/model/service/ConsortiumSearchContextTest.java create mode 100644 src/test/java/org/folio/search/sample/SampleInstancesResponse.java create mode 100644 src/test/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilderTest.java create mode 100644 src/test/resources/samples/instance-response-sample/instance-basic-response.json create mode 100644 src/test/resources/samples/instance-response-sample/instance-full-response.json diff --git a/src/main/java/org/folio/search/configuration/WebConfig.java b/src/main/java/org/folio/search/configuration/WebConfig.java index 6fb656fb3..ae4e822b8 100644 --- a/src/main/java/org/folio/search/configuration/WebConfig.java +++ b/src/main/java/org/folio/search/configuration/WebConfig.java @@ -4,6 +4,7 @@ import org.folio.search.domain.dto.BrowseType; import org.folio.search.domain.dto.CallNumberType; import org.folio.search.domain.dto.RecordType; +import org.folio.search.domain.dto.SortOrder; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.format.FormatterRegistry; @@ -18,6 +19,7 @@ public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToCallNumberTypeEnumConverter()); registry.addConverter(new StringToBrowseTypeConverter()); registry.addConverter(new StringToBrowseOptionTypeConverter()); + registry.addConverter(new StringToSortOrderTypeConverter()); } private static final class StringToRecordTypeEnumConverter implements Converter { @@ -47,4 +49,11 @@ public BrowseOptionType convert(String source) { return BrowseOptionType.fromValue(source.toLowerCase()); } } + + private static final class StringToSortOrderTypeConverter implements Converter { + @Override + public SortOrder convert(String source) { + return SortOrder.fromValue(source.toLowerCase()); + } + } } diff --git a/src/main/java/org/folio/search/controller/SearchConsortiumController.java b/src/main/java/org/folio/search/controller/SearchConsortiumController.java new file mode 100644 index 000000000..e5813ab7c --- /dev/null +++ b/src/main/java/org/folio/search/controller/SearchConsortiumController.java @@ -0,0 +1,54 @@ +package org.folio.search.controller; + +import lombok.RequiredArgsConstructor; +import org.folio.search.domain.dto.ConsortiumHoldingCollection; +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.ConsortiumTenantService; +import org.folio.spring.integration.XOkapiHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/") +public class SearchConsortiumController implements SearchConsortiumApi { + + static final String REQUEST_NOT_ALLOWED_MSG = + "The request allowed only for central tenant of consortium environment"; + + private final ConsortiumTenantService consortiumTenantService; + private final ConsortiumInstanceService instanceService; + + @Override + public ResponseEntity getConsortiumHoldings(String tenantHeader, String instanceId, + String tenantId, Integer limit, + Integer offset, String sortBy, + SortOrder sortOrder) { + checkAllowance(tenantHeader); + var context = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS) + .filter("instanceId", instanceId) + .filter("tenantId", tenantId) + .limit(limit) + .offset(offset) + .sortBy(sortBy) + .sortOrder(sortOrder) + .build(); + return ResponseEntity.ok(instanceService.fetchHoldings(context)); + } + + private void checkAllowance(String tenantHeader) { + var centralTenant = consortiumTenantService.getCentralTenant(tenantHeader); + if (centralTenant.isEmpty() || !centralTenant.get().equals(tenantHeader)) { + throw new RequestValidationException(REQUEST_NOT_ALLOWED_MSG, XOkapiHeaders.TENANT, tenantHeader); + } + } + +} diff --git a/src/main/java/org/folio/search/model/service/ConsortiumSearchContext.java b/src/main/java/org/folio/search/model/service/ConsortiumSearchContext.java new file mode 100644 index 000000000..332df9f52 --- /dev/null +++ b/src/main/java/org/folio/search/model/service/ConsortiumSearchContext.java @@ -0,0 +1,100 @@ +package org.folio.search.model.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.exception.RequestValidationException; +import org.folio.search.model.Pair; +import org.folio.search.model.types.ResourceType; + +@Getter +public class ConsortiumSearchContext { + + static final String SORT_NOT_ALLOWED_MSG = "Not allowed sort field for %s"; + static final String FILTER_REQUIRED_MSG = "At least one filter criteria required"; + + private static final Map> ALLOWED_SORT_FIELDS = Map.of( + ResourceType.HOLDINGS, List.of("id", "hrid", "tenantId", "instanceId", + "callNumberPrefix", "callNumber", "copyNumber", "permanentLocationId") + ); + + private static final Map DEFAULT_SORT_FIELD = Map.of( + ResourceType.HOLDINGS, "id" + ); + + private final ResourceType resourceType; + private final List> filters; + private final Integer limit; + private final Integer offset; + private final String sortBy; + private final SortOrder sortOrder; + + ConsortiumSearchContext(ResourceType resourceType, List> filters, Integer limit, Integer offset, + String sortBy, SortOrder sortOrder) { + this.resourceType = resourceType; + this.filters = filters; + if (sortBy != null && !ALLOWED_SORT_FIELDS.get(resourceType).contains(sortBy)) { + throw new RequestValidationException(SORT_NOT_ALLOWED_MSG.formatted(resourceType.getValue()), "sortBy", sortBy); + } + if (sortBy != null && (filters.isEmpty())) { + throw new RequestValidationException(FILTER_REQUIRED_MSG, null, null); + } + this.limit = limit; + this.offset = offset; + this.sortBy = sortBy; + this.sortOrder = sortOrder; + } + + public static ConsortiumSearchContextBuilder builderFor(ResourceType resourceType) { + return new ConsortiumSearchContextBuilder(resourceType); + } + + public static class ConsortiumSearchContextBuilder { + private final ResourceType resourceType; + private List> filters = new ArrayList<>(); + private Integer limit; + private Integer offset; + private String sortBy; + private SortOrder sortOrder; + + ConsortiumSearchContextBuilder(ResourceType resourceType) { + this.resourceType = resourceType; + } + + public ConsortiumSearchContextBuilder filter(String name, String value) { + if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value)) { + this.filters.add(Pair.pair(name, value)); + } + return this; + } + + public ConsortiumSearchContextBuilder limit(Integer limit) { + this.limit = limit; + return this; + } + + public ConsortiumSearchContextBuilder offset(Integer offset) { + this.offset = offset; + return this; + } + + public ConsortiumSearchContextBuilder sortBy(String sortBy) { + this.sortBy = sortBy; + return this; + } + + public ConsortiumSearchContextBuilder sortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + return this; + } + + public ConsortiumSearchContext build() { + return new ConsortiumSearchContext(this.resourceType, this.filters, this.limit, this.offset, + this.sortBy, this.sortOrder); + } + } +} + diff --git a/src/main/java/org/folio/search/model/types/ResourceType.java b/src/main/java/org/folio/search/model/types/ResourceType.java index 9f7c22f85..2dc260693 100644 --- a/src/main/java/org/folio/search/model/types/ResourceType.java +++ b/src/main/java/org/folio/search/model/types/ResourceType.java @@ -6,13 +6,13 @@ public enum ResourceType { INSTANCE("instance"), + HOLDINGS("holdings"), AUTHORITY("authority"), CLASSIFICATION_TYPE("classification-type"); private final String value; ResourceType(String value) { - this.value = value; } } diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceRepository.java b/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceRepository.java index 363587371..93b0a35da 100644 --- a/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceRepository.java +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceRepository.java @@ -1,5 +1,6 @@ package org.folio.search.service.consortium; +import static org.folio.search.service.consortium.ConsortiumSearchQueryBuilder.CONSORTIUM_TABLES; import static org.folio.search.utils.JdbcUtils.getFullTableName; import static org.folio.search.utils.JdbcUtils.getParamPlaceholder; @@ -13,6 +14,8 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.folio.search.domain.dto.ConsortiumHolding; +import org.folio.search.model.types.ResourceType; import org.folio.spring.FolioExecutionContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -22,7 +25,6 @@ @RequiredArgsConstructor public class ConsortiumInstanceRepository { - static final String CONSORTIUM_INSTANCE_TABLE_NAME = "consortium_instance"; private static final String SELECT_BY_ID_SQL = "SELECT * FROM %s WHERE instance_id IN (%s)"; private static final String DELETE_BY_TENANT_AND_ID_SQL = "DELETE FROM %s WHERE tenant_id = ? AND instance_id = ?;"; private static final String UPSERT_SQL = """ @@ -74,12 +76,27 @@ public void delete(Set instanceIds) { ); } + public List fetchHoldings(ConsortiumSearchQueryBuilder searchQueryBuilder) { + return jdbcTemplate.query(con -> searchQueryBuilder.buildSelectQuery(context, con), + (rs, rowNum) -> new ConsortiumHolding() + .id(rs.getString("id")) + .hrid(rs.getString("hrid")) + .tenantId(rs.getString("tenantId")) + .instanceId(rs.getString("instanceId")) + .callNumberPrefix(rs.getString("callNumberPrefix")) + .callNumber(rs.getString("callNumber")) + .copyNumber(rs.getString("copyNumber")) + .permanentLocationId(rs.getString("permanentLocationId")) + .discoverySuppress(rs.getBoolean("discoverySuppress")) + ); + } + private ConsortiumInstance toConsortiumInstance(ResultSet rs) throws SQLException { var id = new ConsortiumInstanceId(rs.getString(TENANT_ID_COLUMN), rs.getString(INSTANCE_ID_COLUMN)); return new ConsortiumInstance(id, rs.getString(JSON_COLUMN)); } private String getTableName() { - return getFullTableName(context, CONSORTIUM_INSTANCE_TABLE_NAME); + return getFullTableName(context, CONSORTIUM_TABLES.get(ResourceType.INSTANCE)); } } diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceService.java b/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceService.java index 711864965..fa65f9480 100644 --- a/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceService.java +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumInstanceService.java @@ -14,9 +14,12 @@ import lombok.extern.log4j.Log4j2; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.ListUtils; +import org.folio.search.domain.dto.ConsortiumHolding; +import org.folio.search.domain.dto.ConsortiumHoldingCollection; import org.folio.search.domain.dto.ResourceEvent; import org.folio.search.domain.dto.ResourceEventType; import org.folio.search.model.event.ConsortiumInstanceEvent; +import org.folio.search.model.service.ConsortiumSearchContext; import org.folio.search.utils.JsonConverter; import org.folio.search.utils.SearchConverterUtils; import org.folio.spring.FolioExecutionContext; @@ -147,6 +150,11 @@ public List fetchInstances(Iterable instanceIds) { return resourceEvents; } + public ConsortiumHoldingCollection fetchHoldings(ConsortiumSearchContext context) { + List holdingList = repository.fetchHoldings(new ConsortiumSearchQueryBuilder(context)); + return new ConsortiumHoldingCollection().holdings(holdingList).totalRecords(holdingList.size()); + } + @SuppressWarnings("unchecked") private void addListItems(List> mergedList, Map instanceMap, String key) { var items = instanceMap.get(key); diff --git a/src/main/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilder.java b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilder.java new file mode 100644 index 000000000..23a8dda48 --- /dev/null +++ b/src/main/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilder.java @@ -0,0 +1,125 @@ +package org.folio.search.service.consortium; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.SPACE; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.wrap; +import static org.folio.search.utils.JdbcUtils.getFullTableName; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.model.Pair; +import org.folio.search.model.service.ConsortiumSearchContext; +import org.folio.search.model.types.ResourceType; +import org.folio.spring.FolioExecutionContext; + +public class ConsortiumSearchQueryBuilder { + + static final String CONSORTIUM_INSTANCE_TABLE_NAME = "consortium_instance"; + public static final Map CONSORTIUM_TABLES = Map.of( + ResourceType.INSTANCE, CONSORTIUM_INSTANCE_TABLE_NAME, + ResourceType.HOLDINGS, CONSORTIUM_INSTANCE_TABLE_NAME + ); + private static final Map> RESOURCE_FIELDS = Map.of( + ResourceType.HOLDINGS, + List.of("id", "hrid", "callNumberPrefix", "callNumber", "copyNumber", "permanentLocationId", "discoverySuppress") + ); + + private static final Map> RESOURCE_FILTER_DATABASE_NAME = Map.of( + ResourceType.HOLDINGS, Map.of("instanceId", "instance_id", "tenantId", "tenant_id") + ); + private final ConsortiumSearchContext searchContext; + private final List> filters; + + public ConsortiumSearchQueryBuilder(ConsortiumSearchContext searchContext) { + this.searchContext = searchContext; + this.filters = getFilters(searchContext.getResourceType()); + } + + public String buildSelectQuery(FolioExecutionContext context) { + var resourceType = searchContext.getResourceType(); + var fullTableName = getFullTableName(context, CONSORTIUM_TABLES.get(resourceType)); + String subQuery = "SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings FROM " + + fullTableName + SPACE + getWhereClause(filters); + String query = "SELECT i.instance_id as instanceId, i.tenant_id as tenantId," + + getSelectors("i.holdings", RESOURCE_FIELDS.get(resourceType)) + + " FROM (" + subQuery + ") i" + + getOrderByClause() + + getLimitClause() + + getOffsetClause(); + return StringUtils.normalizeSpace(query); + } + + public PreparedStatement buildSelectQuery(FolioExecutionContext context, Connection con) throws SQLException { + var preparedStatement = con.prepareStatement(buildSelectQuery(context)); + for (int i = 0; i < filters.size(); i++) { + preparedStatement.setString(i + 1, filters.get(i).getSecond()); + } + return preparedStatement; + } + + private String getOffsetClause() { + if (searchContext.getOffset() == null) { + return EMPTY; + } + return wrapped("OFFSET " + searchContext.getOffset()); + } + + private String wrapped(String str) { + return wrap(str, ' '); + } + + private String getLimitClause() { + if (searchContext.getLimit() == null) { + return EMPTY; + } + return wrapped("LIMIT " + searchContext.getLimit()); + } + + private String getOrderByClause() { + var sortBy = searchContext.getSortBy(); + if (isBlank(sortBy)) { + return EMPTY; + } + var sortOrder = searchContext.getSortOrder(); + return wrapped("ORDER BY " + sortBy + SPACE + (sortOrder == null ? EMPTY : sortOrder.getValue())); + } + + private String getJsonSelector(String source, String field) { + return source + " ->> " + wrap(field, '\''); + } + + private String getSelectors(String source, List sourceFields) { + return wrap(sourceFields.stream() + .map(field -> getJsonSelector(source, field) + " AS " + field) + .collect(Collectors.joining(", ")), ' '); + } + + private String getWhereClause(List> filters) { + if (filters.isEmpty()) { + return EMPTY; + } + var conditionsClause = filters.stream() + .map(filter -> filter.getFirst() + " = ?") + .collect(Collectors.joining(" AND ")); + return conditionsClause.isBlank() ? conditionsClause : "WHERE " + conditionsClause; + } + + private List> getFilters(ResourceType resourceType) { + var mappedFilterNames = RESOURCE_FILTER_DATABASE_NAME.get(resourceType); + return searchContext.getFilters().stream() + .map(filter -> { + if (mappedFilterNames.containsKey(filter.getFirst())) { + return Pair.pair(mappedFilterNames.get(filter.getFirst()), filter.getSecond()); + } + return filter; + }).toList(); + } + +} + diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml index f0460663a..2a33c0487 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -202,6 +202,32 @@ paths: '500': $ref: '#/components/responses/internalServerErrorResponse' + /search/consortium/holdings: + get: + operationId: getConsortiumHoldings + description: Get a list of holdings (only for consortium environment) + tags: + - search-consortium + parameters: + - $ref: '#/components/parameters/instance-id-query-param' + - $ref: '#/components/parameters/tenant-id-query-param' + - $ref: '#/components/parameters/consortium-limit-param' + - $ref: '#/components/parameters/offset-param' + - $ref: '#/components/parameters/sort-by-holdings-param' + - $ref: '#/components/parameters/sort-order-param' + - $ref: '#/components/parameters/x-okapi-tenant-header' + responses: + '200': + description: List of holdings + content: + application/json: + schema: + $ref: '#/components/schemas/consortiumHoldingCollection' + '400': + $ref: '#/components/responses/badRequestResponse' + '500': + $ref: '#/components/responses/internalServerErrorResponse' + /browse/call-numbers/instances: get: operationId: browseInstancesByCallNumber @@ -725,6 +751,50 @@ components: $ref: '#/components/schemas/browseConfig' totalRecords: type: integer + consortiumHolding: + type: object + properties: + id: + description: Holdings ID + type: string + hrid: + description: Holdings HRID + type: string + tenantId: + description: Tenant ID of the Holding + type: string + instanceId: + description: Related Instance Id + type: string + discoverySuppress: + description: Discovery suppress flag + type: boolean + callNumberPrefix: + description: Call number prefix + type: string + callNumber: + description: Call number + type: string + copyNumber: + description: Copy number + type: string + permanentLocationId: + description: Permanent Location ID + type: string + consortiumHoldingCollection: + type: object + properties: + holdings: + type: array + items: + $ref: '#/components/schemas/consortiumHolding' + totalRecords: + type: integer + sortOrder: + type: string + enum: + - asc + - desc responses: unprocessableEntityResponse: @@ -756,6 +826,15 @@ components: minimum: 0 maximum: 500 default: 100 + consortium-limit-param: + in: query + name: limit + description: Limit the number of elements returned in the response. + schema: + type: integer + minimum: 0 + maximum: 1000 + default: 100 browse-limit-param: in: query name: limit @@ -855,6 +934,44 @@ components: required: true schema: type: string + instance-id-query-param: + in: query + name: instanceId + description: UUID of the instance + required: false + schema: + type: string + tenant-id-query-param: + in: query + name: tenantId + description: Tenant ID + required: false + schema: + type: string + sort-by-holdings-param: + in: query + name: sortBy + description: | + Defines a field to sort by. + Possible values: + - id + - hrid + - tenantId + - instanceId + - callNumberPrefix + - callNumber + - copyNumber + - permanentLocationId + required: false + schema: + type: string + sort-order-param: + in: query + name: sortOrder + description: Defines sorting order + required: false + schema: + $ref: '#/components/schemas/sortOrder' include-number-of-titles: in: query name: includeNumberOfTitles diff --git a/src/main/resources/swagger.api/schemas/holding.json b/src/main/resources/swagger.api/schemas/holding.json index 0a4ddd99d..33014c4f3 100644 --- a/src/main/resources/swagger.api/schemas/holding.json +++ b/src/main/resources/swagger.api/schemas/holding.json @@ -62,6 +62,10 @@ "type": "string", "description": "Suffix of the call number on the holding level." }, + "copyNumber": { + "type": "string", + "description": "Item/Piece ID (usually barcode) for systems that do not use item records." + }, "electronicAccess": { "type": "array", "description": "List of electronic access items", diff --git a/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java index a054c8910..d8d73b15a 100644 --- a/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseClassificationConsortiumIT.java @@ -10,7 +10,7 @@ import static org.folio.search.support.base.ApiEndpoints.instanceClassificationBrowsePath; import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; import static org.folio.search.utils.SearchUtils.getIndexName; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +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.TestUtils.classificationBrowseItem; import static org.folio.search.utils.TestUtils.classificationBrowseResult; @@ -47,11 +47,11 @@ class BrowseClassificationConsortiumIT extends BaseConsortiumIntegrationTest { @BeforeAll static void prepare() { - setUpTenant(CONSORTIUM_TENANT_ID); + setUpTenant(CENTRAL_TENANT_ID); setUpTenant(MEMBER_TENANT_ID); - saveRecords(CONSORTIUM_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), + saveRecords(CENTRAL_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), INSTANCES_CENTRAL.length, - instance -> inventoryApi.createInstance(CONSORTIUM_TENANT_ID, instance)); + instance -> inventoryApi.createInstance(CENTRAL_TENANT_ID, instance)); saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), asList(INSTANCES_MEMBER), INSTANCES_CENTRAL.length + INSTANCES_MEMBER.length, instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance)); @@ -59,7 +59,7 @@ static void prepare() { await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { var searchRequest = new SearchRequest() .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(100)) - .indices(getIndexName(SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE, CONSORTIUM_TENANT_ID)); + .indices(getIndexName(SearchUtils.INSTANCE_CLASSIFICATION_RESOURCE, CENTRAL_TENANT_ID)); var searchResponse = elasticClient.search(searchRequest, RequestOptions.DEFAULT); assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(17); }); diff --git a/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java index 58bf45bf5..dfba34280 100644 --- a/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseContributorConsortiumIT.java @@ -10,7 +10,7 @@ import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; import static org.folio.search.support.base.ApiEndpoints.recordFacetsPath; import static org.folio.search.utils.SearchUtils.getIndexName; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +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.TestUtils.array; import static org.folio.search.utils.TestUtils.contributorBrowseItem; @@ -66,11 +66,11 @@ class BrowseContributorConsortiumIT extends BaseConsortiumIntegrationTest { @BeforeAll static void prepare(@Autowired RestHighLevelClient restHighLevelClient) throws InterruptedException { - setUpTenant(CONSORTIUM_TENANT_ID); + setUpTenant(CENTRAL_TENANT_ID); setUpTenant(MEMBER_TENANT_ID); - saveRecords(CONSORTIUM_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), + saveRecords(CENTRAL_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), INSTANCES_CENTRAL.length, - instance -> inventoryApi.createInstance(CONSORTIUM_TENANT_ID, instance)); + instance -> inventoryApi.createInstance(CENTRAL_TENANT_ID, instance)); saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), asList(INSTANCES_MEMBER), INSTANCES_CENTRAL.length + INSTANCES_MEMBER.length, instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance)); @@ -78,12 +78,12 @@ static void prepare(@Autowired RestHighLevelClient restHighLevelClient) throws I // this is needed to test deleting contributors when all instances are unlinked from a contributor var instanceToUpdate = INSTANCES_CENTRAL[0]; instanceToUpdate.setContributors(Collections.emptyList()); - inventoryApi.updateInstance(CONSORTIUM_TENANT_ID, instanceToUpdate); + inventoryApi.updateInstance(CENTRAL_TENANT_ID, instanceToUpdate); await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { var searchRequest = new SearchRequest() .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(0)) - .indices(getIndexName(SearchUtils.CONTRIBUTOR_RESOURCE, CONSORTIUM_TENANT_ID)); + .indices(getIndexName(SearchUtils.CONTRIBUTOR_RESOURCE, CENTRAL_TENANT_ID)); var searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(12); }); @@ -149,7 +149,7 @@ private static Stream facetQueriesProvider() { facet(facetItem("false", 8), facetItem("true", 5)))), arguments("cql.allRecords=1", array("instances.tenantId"), mapOf("instances.tenantId", facet(facetItem(MEMBER_TENANT_ID, 8), - facetItem(CONSORTIUM_TENANT_ID, 5)))) + facetItem(CENTRAL_TENANT_ID, 5)))) ); } diff --git a/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java b/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java index 62e0c4488..18a169e54 100644 --- a/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/BrowseSubjectConsortiumIT.java @@ -11,7 +11,7 @@ import static org.folio.search.support.base.ApiEndpoints.instanceSubjectBrowsePath; import static org.folio.search.support.base.ApiEndpoints.recordFacetsPath; import static org.folio.search.utils.SearchUtils.getIndexName; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +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.TestUtils.array; import static org.folio.search.utils.TestUtils.facet; @@ -62,11 +62,11 @@ class BrowseSubjectConsortiumIT extends BaseConsortiumIntegrationTest { @BeforeAll static void prepare(@Autowired RestHighLevelClient restHighLevelClient) { - setUpTenant(CONSORTIUM_TENANT_ID); + setUpTenant(CENTRAL_TENANT_ID); setUpTenant(MEMBER_TENANT_ID); - saveRecords(CONSORTIUM_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), + saveRecords(CENTRAL_TENANT_ID, instanceSearchPath(), asList(INSTANCES_CENTRAL), INSTANCES_CENTRAL.length, - instance -> inventoryApi.createInstance(CONSORTIUM_TENANT_ID, instance)); + instance -> inventoryApi.createInstance(CENTRAL_TENANT_ID, instance)); saveRecords(MEMBER_TENANT_ID, instanceSearchPath(), asList(INSTANCES_MEMBER), INSTANCES_CENTRAL.length + INSTANCES_MEMBER.length, instance -> inventoryApi.createInstance(MEMBER_TENANT_ID, instance)); @@ -74,7 +74,7 @@ static void prepare(@Autowired RestHighLevelClient restHighLevelClient) { await().atMost(ONE_MINUTE).pollInterval(ONE_SECOND).untilAsserted(() -> { var searchRequest = new SearchRequest() .source(searchSource().query(matchAllQuery()).trackTotalHits(true).from(0).size(100)) - .indices(getIndexName(SearchUtils.INSTANCE_SUBJECT_RESOURCE, CONSORTIUM_TENANT_ID)); + .indices(getIndexName(SearchUtils.INSTANCE_SUBJECT_RESOURCE, CENTRAL_TENANT_ID)); var searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); assertThat(searchResponse.getHits().getTotalHits().value).isEqualTo(23); System.out.println("Resulted subjects"); @@ -140,7 +140,7 @@ private static Stream facetQueriesProvider() { facet(facetItem("false", 15), facetItem("true", 10)))), arguments("cql.allRecords=1", array("instances.tenantId"), mapOf("instances.tenantId", facet(facetItem(MEMBER_TENANT_ID, 15), - facetItem(CONSORTIUM_TENANT_ID, 10)))) + facetItem(CENTRAL_TENANT_ID, 10)))) ); } diff --git a/src/test/java/org/folio/search/controller/SearchHoldingsConsortiumIT.java b/src/test/java/org/folio/search/controller/SearchHoldingsConsortiumIT.java new file mode 100644 index 000000000..ddf0fd02f --- /dev/null +++ b/src/test/java/org/folio/search/controller/SearchHoldingsConsortiumIT.java @@ -0,0 +1,109 @@ +package org.folio.search.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.controller.SearchConsortiumController.REQUEST_NOT_ALLOWED_MSG; +import static org.folio.search.model.Pair.pair; +import static org.folio.search.sample.SampleInstances.getSemanticWeb; +import static org.folio.search.sample.SampleInstances.getSemanticWebId; +import static org.folio.search.support.base.ApiEndpoints.consortiumHoldingsSearchPath; +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.TestUtils.parseResponse; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +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 org.folio.search.domain.dto.ConsortiumHolding; +import org.folio.search.domain.dto.ConsortiumHoldingCollection; +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 SearchHoldingsConsortiumIT extends BaseConsortiumIntegrationTest { + + @BeforeAll + static void prepare() { + setUpTenant(CENTRAL_TENANT_ID); + setUpTenant(MEMBER_TENANT_ID, getSemanticWeb()); + } + + @AfterAll + static void cleanUp() { + removeTenant(); + } + + @Test + void doGetConsortiumHoldings_returns200AndRecords() { + var result = doGet(consortiumHoldingsSearchPath(), CENTRAL_TENANT_ID); + var actual = parseResponse(result, ConsortiumHoldingCollection.class); + + assertThat(actual.getTotalRecords()).isEqualTo(3); + assertThat(actual.getHoldings()).containsExactlyInAnyOrder(getExpectedHoldings()); + } + + @Test + void doGetConsortiumHoldings_returns200AndRecords_withAllQueryParams() { + List> queryParams = List.of( + pair("instanceId", getSemanticWebId()), + pair("tenantId", MEMBER_TENANT_ID), + pair("limit", "1"), + pair("offset", "1"), + pair("sortBy", "callNumber"), + pair("sortOrder", "desc") + ); + var result = doGet(consortiumHoldingsSearchPath(queryParams), CENTRAL_TENANT_ID); + var actual = parseResponse(result, ConsortiumHoldingCollection.class); + + assertThat(actual.getTotalRecords()).isEqualTo(1); + assertThat(actual.getHoldings()) + .satisfiesExactly(input -> assertEquals("call number", input.getCallNumber())); + } + + @Test + void tryGetConsortiumHoldings_returns400_whenRequestedForNotCentralTenant() throws Exception { + tryGet(consortiumHoldingsSearchPath()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message", is(REQUEST_NOT_ALLOWED_MSG))) + .andExpect(jsonPath("$.errors[0].type", is("RequestValidationException"))) + .andExpect(jsonPath("$.errors[0].code", is("validation_error"))) + .andExpect(jsonPath("$.errors[0].parameters[0].key", is("x-okapi-tenant"))) + .andExpect(jsonPath("$.errors[0].parameters[0].value", is(MEMBER_TENANT_ID))); + } + + @Test + void tryGetConsortiumHoldings_returns400_whenOrderBySpecifiedWithoutAnyFilters() throws Exception { + List> queryParams = List.of( + pair("limit", "1"), + pair("offset", "1"), + pair("sortBy", "callNumber"), + pair("sortOrder", "desc") + ); + tryGet(consortiumHoldingsSearchPath(queryParams), CENTRAL_TENANT_ID) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message", is("At least one filter criteria required"))) + .andExpect(jsonPath("$.errors[0].type", is("RequestValidationException"))) + .andExpect(jsonPath("$.errors[0].code", is("validation_error"))); + } + + private ConsortiumHolding[] getExpectedHoldings() { + var instance = getSemanticWeb(); + return instance.getHoldings().stream() + .map(holding -> new ConsortiumHolding() + .id(holding.getId()) + .hrid(holding.getHrid()) + .tenantId(MEMBER_TENANT_ID) + .instanceId(instance.getId()) + .callNumberPrefix(holding.getCallNumberPrefix()) + .callNumber(holding.getCallNumber()) + .copyNumber(holding.getCopyNumber()) + .permanentLocationId(holding.getPermanentLocationId()) + .discoverySuppress(holding.getDiscoverySuppress() != null && holding.getDiscoverySuppress())) + .toArray(ConsortiumHolding[]::new); + } +} diff --git a/src/test/java/org/folio/search/controller/SearchInstanceConsortiumIT.java b/src/test/java/org/folio/search/controller/SearchInstanceConsortiumIT.java index c40c9e551..f157e579b 100644 --- a/src/test/java/org/folio/search/controller/SearchInstanceConsortiumIT.java +++ b/src/test/java/org/folio/search/controller/SearchInstanceConsortiumIT.java @@ -2,38 +2,26 @@ import static org.folio.search.sample.SampleInstances.getSemanticWeb; import static org.folio.search.sample.SampleInstances.getSemanticWebId; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.sample.SampleInstancesResponse.getInstanceBasicResponseSample; +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.TestUtils.parseResponse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.junit.jupiter.api.Assertions.assertEquals; -import com.fasterxml.jackson.core.type.TypeReference; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.folio.search.domain.dto.Holding; import org.folio.search.domain.dto.Instance; -import org.folio.search.domain.dto.Item; -import org.folio.search.domain.dto.ItemEffectiveCallNumberComponents; -import org.folio.search.model.service.ResultList; +import org.folio.search.domain.dto.InstanceSearchResult; import org.folio.search.support.base.BaseConsortiumIntegrationTest; import org.folio.spring.testing.type.IntegrationTest; -import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.provider.Arguments; @IntegrationTest class SearchInstanceConsortiumIT extends BaseConsortiumIntegrationTest { @BeforeAll static void prepare() { - setUpTenant(CONSORTIUM_TENANT_ID); + setUpTenant(CENTRAL_TENANT_ID); setUpTenant(MEMBER_TENANT_ID, getSemanticWeb()); } @@ -43,302 +31,19 @@ static void cleanUp() { } @Test - void responseContainsOnlyBasicInstanceProperties() throws Exception { - var expected = getSemanticWeb(); - var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId())) - .andExpect(jsonPath("totalRecords", is(1))) - // make sure that no unexpected properties are present - .andExpect(jsonPath("instances[0].length()", is(13))); + void responseContainsOnlyBasicInstanceProperties() { + var expected = getInstanceBasicResponseSample(); + setTenant(expected); + var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId())); - var actual = parseResponse(response, new TypeReference>() { }).getResult().get(0); - assertThat(actual.getId(), is(expected.getId())); - assertThat(actual.getTitle(), is(expected.getTitle())); - assertThat(actual.getContributors(), is(expected.getContributors())); - assertThat(actual.getStaffSuppress(), is(false)); - assertThat(actual.getIsBoundWith(), is(true)); - assertThat(actual.getDiscoverySuppress(), is(expected.getDiscoverySuppress())); - assertThat(actual.getPublication(), is(expected.getPublication())); - assertThat(actual.getItems(), is(List.of( - new Item() - .effectiveCallNumberComponents(callNumber("prefix-10101", "suffix-10101")) - .effectiveShelvingOrder("TK 45105.88815 A58 42004 FT MEADE"), - new Item() - .effectiveCallNumberComponents(callNumber("prefix-90000", "suffix-90000")) - .effectiveShelvingOrder("TK 45105.88815 A58 42004 FT MEADE") - ))); - assertThat(actual.getHoldings(), is(List.of())); - assertThat(actual.getElectronicAccess(), is(List.of())); - assertThat(actual.getNotes(), is(List.of())); - } - - @Test - void responseContainsAllInstanceProperties() throws Exception { - var expected = getSemanticWeb(); - var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId()), true) - .andExpect(jsonPath("totalRecords", is(1))); - - var actual = parseResponse(response, new TypeReference>() { }).getResult().get(0); - - assertThat(actual.getHoldings(), containsInAnyOrder(expected.getHoldings().stream() - .map(hr -> hr.discoverySuppress(false)) - .map(SearchInstanceConsortiumIT::removeUnexpectedProperties) - .map(Matchers::is).collect(Collectors.toList()))); - - assertThat(actual.getItems(), containsInAnyOrder(expected.getItems().stream() - .map(SearchInstanceConsortiumIT::removeUnexpectedProperties) - .map(Matchers::is).collect(Collectors.toList()))); - - assertThat(actual.tenantId(null).shared(null).holdings(null).items(null), - is(removeUnexpectedProperties(expected))); - } - - private static Holding removeUnexpectedProperties(Holding holding) { - holding.setTenantId(MEMBER_TENANT_ID); - holding.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - return holding.callNumberSuffix(null).callNumber(null).callNumberPrefix(null); - } - - private static Item removeUnexpectedProperties(Item item) { - item.setTenantId(MEMBER_TENANT_ID); - item.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - return item.discoverySuppress(false); - } - - private static Instance removeUnexpectedProperties(Instance instance) { - instance.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - instance.getClassifications().forEach(c -> c.setClassificationTypeId(null)); - instance.setTenantId(null); - instance.setShared(null); - return instance.staffSuppress(false).discoverySuppress(false).items(null).holdings(null); - } + var actual = parseResponse(response, InstanceSearchResult.class); - private static ItemEffectiveCallNumberComponents callNumber(String prefix, String suffix) { - return new ItemEffectiveCallNumberComponents().prefix(prefix).suffix(suffix) - .callNumber("TK5105.88815 . A58 2004 FT MEADE") - .typeId("512173a7-bd09-490e-b773-17d83f2b63fe"); + assertEquals(expected, actual); } - private static Stream testDataProvider() { - return Stream.of( - arguments("cql.allRecords = 1", getSemanticWebId()), - arguments("id = {value}", "\"\""), - arguments("id = {value}", getSemanticWebId()), - arguments("id = {value}", "5bf370e0*a0a39"), - arguments("id == {value}", getSemanticWebId()), - - arguments("tenantId = {value}", MEMBER_TENANT_ID), - - arguments("shared == {value}", "false"), - - arguments("title <> {value}", "unknown value"), - arguments("indexTitle <> {value}", "unknown value"), - - arguments("title all {value}", "semantic"), - arguments("title all {value}", "primers"), - arguments("title all {value}", "cherchell"), - arguments("title all {value}", "cooperate"), - arguments("title all {value}", "cooperative"), - arguments("title any {value}", "semantic web word"), - arguments("title all {value}", "system information"), - // search by instance title (search across 2 fields) - arguments("title any {value}", "systems alternative semantic"), - - //phrase matching - arguments("title == {value}", "semantic web"), - arguments("title == {value}", "a semantic web primer"), - arguments("title == {value}", "cooperative information systems"), - - // ASCII folding - arguments("title all {value}", "deja vu"), - arguments("title all {value}", "déjà vu"), - arguments("title all {value}", "Algérie"), - // e here should replace e + U + 0301 (Combining Acute Accent) - arguments("title all {value}", "algerie"), - - arguments("series all {value}", "Cooperative information systems"), - arguments("series all {value}", "cooperate"), - arguments("series all {value}", "cooperative"), - - arguments("alternativeTitles.alternativeTitle == {value}", "An alternative title"), - arguments("alternativeTitles.alternativeTitle all {value}", "uniform"), - arguments("alternativeTitles.alternativeTitle all {value}", "deja vu"), - arguments("alternativeTitles.alternativeTitle all {value}", "déjà vu"), - arguments("alternativeTitles.alternativeTitle all {value}", "pangok"), - arguments("alternativeTitles.alternativeTitle all {value}", "pang'ok"), - arguments("alternativeTitles.alternativeTitle all {value}", "pang ok"), - arguments("alternativeTitles.alternativeTitle all {value}", "bangk'asyurangsŭ"), - arguments("alternativeTitles.alternativeTitle all {value}", "asyurangsŭ"), - - arguments("uniformTitle all {value}", "uniform"), - - arguments("identifiers.value == {value}", "0262012103"), - arguments("identifiers.value all {value}", "200306*"), - arguments("identifiers.value all ({value})", "047144250X or 2003065165 or 0000-0000"), - arguments("identifiers.value all ({value})", "047144250X and 2003065165 and 0317-8471"), - arguments("identifiers.identifierTypeId == {value}", "c858e4f2-2b6b-4385-842b-60732ee14abb"), - arguments("identifiers.identifierTypeId == 8261054f-be78-422d-bd51-4ed9f33c3422 " - + "and identifiers.value == {value}", "0262012103"), - - arguments("publisher all {value}", "MIT"), - arguments("publisher all {value}", "mit"), - arguments("publisher all {value}", "press"), - - arguments("contributors all {value}", "frank"), - arguments("contributors all {value}", "Frank"), - arguments("contributors all {value}", "grigoris"), - arguments("contributors all {value}", "Grigoris Antoniou"), - arguments("contributors any {value}", "Grigoris frank"), - arguments("contributors all {value}", "Van Harmelen, Frank"), - arguments("contributors == {value}", "Van Harmelen, Frank"), - arguments("contributors ==/string {value}", "Van Harmelen, Frank"), - arguments("contributors = {value}", "Van Harmelen, Fr*"), - arguments("contributors = {value}", "*rmelen, Frank"), - - arguments("contributors.name = {value}", "Van Harmelen, Frank"), - arguments("contributors.name == {value}", "Van Harmelen"), - arguments("contributors.name ==/string {value}", "Van Harmelen, Frank"), - arguments("contributors.name = {value}", "Van Harmelen, Fr*"), - arguments("contributors.name = {value}", "Anton*"), - arguments("contributors.name = {value}", "*rmelen, Frank"), - - arguments("contributors.authorityId == {value}", "55294032-fcf6-45cc-b6da-4420a61ef72c"), - arguments("authorityId == {value}", "55294032-fcf6-45cc-b6da-4420a61ef72c"), - - arguments("hrid == {value}", "inst000000000022"), - arguments("hrid == {value}", "inst00*"), - arguments("hrid == {value}", "*00022"), - arguments("hrid == {value}", "*00000002*"), - - arguments("keyword = *", ""), - arguments("keyword all {value}", "semantic web primer"), - arguments("keyword all {value}", "semantic Antoniou ocm0012345 047144250X"), - arguments("subjects all {value}", "semantic"), - arguments("subjects ==/string {value}", "semantic web"), - - arguments("tags.tagList all {value}", "book"), - arguments("tags.tagList all {value}", "electronic"), - - arguments("classifications.classificationNumber=={value}", "025.04"), - arguments("classifications.classificationNumber=={value}", "025*"), - - arguments("electronicAccess.uri==\"{value}\"", "https://testlibrary.sample.com/journal/10.1002/(ISSN)1938-3703"), - arguments("electronicAccess.linkText all {value}", "access"), - arguments("electronicAccess.publicNote all {value}", "online"), - arguments("instanceFormatIds == {value}", "7f9c4ac0-fa3d-43b7-b978-3bf0be38c4da"), - - arguments("administrativeNotes == {value}", "original + pcc"), - arguments("administrativeNotes any {value}", "original pcc"), - - arguments("publicNotes all {value}", "development"), - arguments("notes.note == {value}", "Librarian private note"), - arguments("notes.note == {value}", "The development of the Semantic Web,"), - arguments("callNumber = {value}", "\"\""), - - // search by isbn10 - arguments("isbn = {value}", "047144250X"), - arguments("isbn = {value}", "04714*"), - arguments("isbn = {value}", "0471-4*"), - - // search by isbn13 - arguments("isbn = {value}", "9780471442509"), - arguments("isbn = {value}", "978-0471-44250-9"), - arguments("isbn = {value}", "paper"), - arguments("isbn = {value}", "978-0471*"), - arguments("isbn = {value}", "9780471*"), - - arguments("issn = {value}", "0317-8471"), - arguments("issn = {value}", "0317*"), - - // search by oclc - arguments("oclc = {value}", "12345 800630"), - arguments("oclc = {value}", "ocm60710867"), - arguments("oclc = {value}", "60710*"), - - // search by item fields - arguments("item.hrid = {value}", "item000000000014"), - arguments("item.hrid = {value}", "item*"), - arguments("item.hrid = {value}", "*00014"), - arguments("item.hrid = {value}", "item*00014"), - - arguments("itemPublicNotes all {value}", "bibliographical references"), - arguments("itemPublicNotes all {value}", "public circulation note"), - - arguments("itemIdentifiers all {value}", "item000000000014"), - arguments("itemIdentifiers all {value}", "81ae0f60-f2bc-450c-84c8-5a21096daed9"), - arguments("itemIdentifiers all {value}", "item_accession_number"), - arguments("itemIdentifiers all {value}", "7212ba6a-8dcf-45a1-be9a-ffaa847c4423"), - arguments("itemIdentifiers all {value}", "itemIdentifierFieldValue"), - - arguments("item.administrativeNotes == {value}", "need attention"), - arguments("item.administrativeNotes all {value}", "v1.1"), - - arguments("item.notes.note == {value}", "Librarian public note for item"), - arguments("item.notes.note == {value}", "Librarian private note for item"), - arguments("item.notes.note == {value}", "testCirculationNote"), - arguments("item.notes.note == {value}", "private circulation note"), - - arguments("item.circulationNotes.note == {value}", "testCirculationNote"), - arguments("item.circulationNotes.note all {value}", "public circulation note"), - arguments("item.circulationNotes.note all {value}", "private circulation note"), - arguments("item.circulationNotes.note all {value}", "*Note"), - arguments("item.circulationNotes.note all {value}", "private circulation*"), - - arguments("item.electronicAccess==\"{value}\"", "table"), - arguments("item.electronicAccess.uri==\"{value}\"", "https://www.loc.gov/catdir/toc/ecip0718/2007020429.html"), - arguments("item.electronicAccess.linkText all {value}", "links available"), - arguments("item.electronicAccess.publicNote all {value}", "table of contents"), - - arguments("item.effectiveShelvingOrder ==/string \"{value}\"", "TK 45105.88815 A58 42004 FT MEADE"), - arguments("itemEffectiveShelvingOrder ==/string \"{value}\"", "TK 45105.88815 A58 42004 FT MEADE"), - - // Search by item fields (Backward compatibility) - arguments("items.hrid = {value}", "item000000000014"), - arguments("items.hrid = {value}", "item*"), - arguments("items.hrid = {value}", "*00014"), - arguments("items.hrid = {value}", "item*00014"), - - arguments("items.notes.note == {value}", "Librarian public note for item"), - arguments("items.notes.note == {value}", "Librarian private note for item"), - arguments("items.notes.note == {value}", "testCirculationNote"), - arguments("items.notes.note == {value}", "private circulation note"), - - arguments("items.tags.tagList all {value}", "item-tag"), - - arguments("items.circulationNotes.note == {value}", "testCirculationNote"), - arguments("items.circulationNotes.note all {value}", "public circulation note"), - arguments("items.circulationNotes.note all {value}", "private circulation note"), - arguments("items.circulationNotes.note all {value}", "*Note"), - arguments("items.circulationNotes.note all {value}", "private circulation*"), - - arguments("items.electronicAccess==\"{value}\"", "table"), - arguments("items.electronicAccess.uri==\"{value}\"", "https://www.loc.gov/catdir/toc/ecip0718/2007020429.html"), - arguments("items.electronicAccess.linkText all {value}", "links available"), - arguments("items.electronicAccess.publicNote all {value}", "table of contents"), - - // search by holding fields - arguments("holdings.administrativeNotes == {value}", "for deletion"), - arguments("holdings.administrativeNotes all {value}", "v2.0"), - arguments("holdingsPublicNotes all {value}", "bibliographical references"), - arguments("holdings.notes.note == {value}", "Librarian public note for holding"), - arguments("holdings.notes.note == {value}", "Librarian private note for holding"), - arguments("holdings.tags.tagList == {value}", "holdings-tag"), - - arguments("holdings.electronicAccess==\"{value}\"", "electronicAccess"), - arguments("holdings.electronicAccess.uri==\"{value}\"", "https://testlibrary.sample.com/holdings?hrid=ho0000006"), - arguments("holdings.electronicAccess.linkText all {value}", "link text"), - arguments("holdings.electronicAccess.publicNote all {value}", "note"), - - arguments("holdingsIdentifiers all {value}", "hold000000000009"), - arguments("holdingsIdentifiers == {value}", "1d76ee84-d776-48d2-ab96-140c24e39ac5"), - arguments("holdingsIdentifiers all {value}", "9b8ec096-fa2e-451b-8e7a-6d1c977ee946"), - arguments("holdingsIdentifiers all {value}", "e3ff6133-b9a2-4d4c-a1c9-dc1867d4df19"), - - //search by multiple different parameters - arguments("(keyword all \"{value}\")", "wolves matthew 9781609383657"), - arguments("(keyword all \"{value}\")", "A semantic web primer : wolves"), - arguments("(keyword all \"{value}\")", "A semantic web primer & wolves"), - arguments("(keyword all \"{value}\")", "A semantic web primer / wolves"), - arguments("(title all \"{value}\")", "A semantic web primer : 0747-0850") - ); + private static void setTenant(InstanceSearchResult expected) { + for (Instance instance : expected.getInstances()) { + instance.setTenantId(MEMBER_TENANT_ID); + } } } diff --git a/src/test/java/org/folio/search/controller/SearchInstanceIT.java b/src/test/java/org/folio/search/controller/SearchInstanceIT.java index d3fb132c0..a8d061079 100644 --- a/src/test/java/org/folio/search/controller/SearchInstanceIT.java +++ b/src/test/java/org/folio/search/controller/SearchInstanceIT.java @@ -1,34 +1,28 @@ package org.folio.search.controller; -import static org.folio.search.sample.SampleInstances.getSemanticWeb; import static org.folio.search.sample.SampleInstances.getSemanticWebAsMap; import static org.folio.search.sample.SampleInstances.getSemanticWebId; +import static org.folio.search.sample.SampleInstancesResponse.getInstanceBasicResponseSample; +import static org.folio.search.sample.SampleInstancesResponse.getInstanceFullResponseSample; import static org.folio.search.support.base.ApiEndpoints.instanceIdsPath; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.parseResponse; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.core.type.TypeReference; -import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; -import org.folio.search.domain.dto.Holding; import org.folio.search.domain.dto.Instance; -import org.folio.search.domain.dto.Item; -import org.folio.search.domain.dto.ItemEffectiveCallNumberComponents; -import org.folio.search.model.service.ResultList; +import org.folio.search.domain.dto.InstanceSearchResult; import org.folio.search.support.base.BaseIntegrationTest; import org.folio.spring.client.AuthnClient; import org.folio.spring.client.PermissionsClient; import org.folio.spring.client.UsersClient; import org.folio.spring.testing.type.IntegrationTest; -import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -114,77 +108,23 @@ void search_negative_unknownField() throws Exception { } @Test - void responseContainsOnlyBasicInstanceProperties() throws Exception { - var expected = getSemanticWeb(); - var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId())) - .andExpect(jsonPath("totalRecords", is(1))) - // make sure that no unexpected properties are present - .andExpect(jsonPath("instances[0].length()", is(13))); - - var actual = parseResponse(response, new TypeReference>() { }).getResult().get(0); - assertThat(actual.getId(), is(expected.getId())); - assertThat(actual.getTitle(), is(expected.getTitle())); - assertThat(actual.getContributors(), is(expected.getContributors())); - assertThat(actual.getStaffSuppress(), is(false)); - assertThat(actual.getIsBoundWith(), is(true)); - assertThat(actual.getDiscoverySuppress(), is(expected.getDiscoverySuppress())); - assertThat(actual.getPublication(), is(expected.getPublication())); - assertThat(actual.getItems(), is(List.of( - new Item() - .effectiveCallNumberComponents(callNumber("prefix-10101", "suffix-10101")) - .effectiveShelvingOrder("TK 45105.88815 A58 42004 FT MEADE"), - new Item() - .effectiveCallNumberComponents(callNumber("prefix-90000", "suffix-90000")) - .effectiveShelvingOrder("TK 45105.88815 A58 42004 FT MEADE") - ))); - assertThat(actual.getHoldings(), is(List.of())); - assertThat(actual.getElectronicAccess(), is(List.of())); - assertThat(actual.getNotes(), is(List.of())); - } - - @Test - void responseContainsAllInstanceProperties() throws Exception { - var expected = getSemanticWeb(); - var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId()), true) - .andExpect(jsonPath("totalRecords", is(1))); - - var actual = parseResponse(response, new TypeReference>() { }).getResult().get(0); - - assertThat(actual.getHoldings(), containsInAnyOrder(expected.getHoldings().stream() - .map(hr -> hr.discoverySuppress(false)) - .map(SearchInstanceIT::removeUnexpectedProperties) - .map(Matchers::is).collect(Collectors.toList()))); + void responseContainsOnlyBasicInstanceProperties() { + var expected = getInstanceBasicResponseSample(); + var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId())); - assertThat(actual.getItems(), containsInAnyOrder(expected.getItems().stream() - .map(SearchInstanceIT::removeUnexpectedProperties) - .map(Matchers::is).collect(Collectors.toList()))); + var actual = parseResponse(response, InstanceSearchResult.class); - assertThat(actual.tenantId(null).shared(null).holdings(null).items(null), - is(removeUnexpectedProperties(expected))); + assertEquals(expected, actual); } - private static Holding removeUnexpectedProperties(Holding holding) { - holding.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - return holding.callNumberSuffix(null).callNumber(null).callNumberPrefix(null); - } - - private static Item removeUnexpectedProperties(Item item) { - item.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - return item.discoverySuppress(false); - } + @Test + void responseContainsAllInstanceProperties() { + var expected = getInstanceFullResponseSample(); + var response = doSearchByInstances(prepareQuery("id=={value}", getSemanticWebId()), true); - private static Instance removeUnexpectedProperties(Instance instance) { - instance.getElectronicAccess().forEach(e -> e.setMaterialsSpecification(null)); - instance.getClassifications().forEach(c -> c.setClassificationTypeId(null)); - instance.setTenantId(null); - instance.setShared(null); - return instance.staffSuppress(false).discoverySuppress(false).items(null).holdings(null); - } + var actual = parseResponse(response, InstanceSearchResult.class); - private static ItemEffectiveCallNumberComponents callNumber(String prefix, String suffix) { - return new ItemEffectiveCallNumberComponents().prefix(prefix).suffix(suffix) - .callNumber("TK5105.88815 . A58 2004 FT MEADE") - .typeId("512173a7-bd09-490e-b773-17d83f2b63fe"); + assertEquals(expected, actual); } private static Stream testDataProvider() { @@ -246,7 +186,7 @@ private static Stream testDataProvider() { arguments("identifiers.value all ({value})", "047144250X and 2003065165 and 0317-8471"), arguments("identifiers.identifierTypeId == {value}", "c858e4f2-2b6b-4385-842b-60732ee14abb"), arguments("identifiers.identifierTypeId == 8261054f-be78-422d-bd51-4ed9f33c3422 " - + "and identifiers.value == {value}", "0262012103"), + + "and identifiers.value == {value}", "0262012103"), arguments("publisher all {value}", "MIT"), arguments("publisher all {value}", "mit"), diff --git a/src/test/java/org/folio/search/model/service/ConsortiumSearchContextTest.java b/src/test/java/org/folio/search/model/service/ConsortiumSearchContextTest.java new file mode 100644 index 000000000..f9fd2e68e --- /dev/null +++ b/src/test/java/org/folio/search/model/service/ConsortiumSearchContextTest.java @@ -0,0 +1,52 @@ +package org.folio.search.model.service; + +import static org.folio.search.model.service.ConsortiumSearchContext.FILTER_REQUIRED_MSG; +import static org.folio.search.model.service.ConsortiumSearchContext.SORT_NOT_ALLOWED_MSG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.exception.RequestValidationException; +import org.folio.search.model.Pair; +import org.folio.search.model.types.ResourceType; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; + +@UnitTest +class ConsortiumSearchContextTest { + + @Test + public void testBuilder_success() { + ConsortiumSearchContext consContext = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS) + .filter("name", "value") + .limit(10) + .offset(1) + .sortBy("id") + .sortOrder(SortOrder.DESC) + .build(); + + assertEquals(ResourceType.HOLDINGS, consContext.getResourceType()); + assertEquals(List.of(Pair.pair("name", "value")), consContext.getFilters()); + assertEquals(10, consContext.getLimit()); + assertEquals(1, consContext.getOffset()); + assertEquals("id", consContext.getSortBy()); + assertEquals(SortOrder.DESC, consContext.getSortOrder()); + } + + @Test + public void testBuilder_error_filterIsRequired() { + var searchContextBuilder = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS).sortBy("id"); + Exception exception = assertThrows(RequestValidationException.class, searchContextBuilder::build); + assertEquals(FILTER_REQUIRED_MSG, exception.getMessage()); + } + + @Test + public void testBuilder_error_sortNotAllowed() { + var searchContextBuilder = ConsortiumSearchContext.builderFor(ResourceType.HOLDINGS) + .filter("name", "value") + .sortBy("wrongField"); + Exception exception = assertThrows(RequestValidationException.class, searchContextBuilder::build); + assertEquals(SORT_NOT_ALLOWED_MSG.formatted(ResourceType.HOLDINGS.getValue()), exception.getMessage()); + } +} diff --git a/src/test/java/org/folio/search/sample/SampleInstancesResponse.java b/src/test/java/org/folio/search/sample/SampleInstancesResponse.java new file mode 100644 index 000000000..ccb65a073 --- /dev/null +++ b/src/test/java/org/folio/search/sample/SampleInstancesResponse.java @@ -0,0 +1,31 @@ +package org.folio.search.sample; + +import static org.folio.search.utils.JsonConverter.MAP_TYPE_REFERENCE; +import static org.folio.search.utils.TestUtils.OBJECT_MAPPER; +import static org.folio.search.utils.TestUtils.readJsonFromFile; + +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.folio.search.domain.dto.InstanceSearchResult; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SampleInstancesResponse { + + public static InstanceSearchResult getInstanceBasicResponseSample() { + return convertMap(readSampleInstanceResponse("instance-basic-response")); + } + + public static InstanceSearchResult getInstanceFullResponseSample() { + return convertMap(readSampleInstanceResponse("instance-full-response")); + } + + private static InstanceSearchResult convertMap(Map instanceFullResponseObject) { + return OBJECT_MAPPER.convertValue(instanceFullResponseObject, InstanceSearchResult.class); + } + + private static Map readSampleInstanceResponse(String sampleName) { + var path = "/samples/instance-response-sample/"; + return readJsonFromFile(path + sampleName + ".json", MAP_TYPE_REFERENCE); + } +} diff --git a/src/test/java/org/folio/search/service/IndexServiceTest.java b/src/test/java/org/folio/search/service/IndexServiceTest.java index 0585055de..ed395480a 100644 --- a/src/test/java/org/folio/search/service/IndexServiceTest.java +++ b/src/test/java/org/folio/search/service/IndexServiceTest.java @@ -9,7 +9,7 @@ import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE; import static org.folio.search.utils.SearchUtils.getIndexName; import static org.folio.search.utils.SearchUtils.getResourceName; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.EMPTY_JSON_OBJECT; import static org.folio.search.utils.TestConstants.EMPTY_OBJECT; import static org.folio.search.utils.TestConstants.INDEX_NAME; @@ -267,7 +267,7 @@ void reindexInventory_positive_recreateIndexIsTrue_memberTenant() { when(resourceReindexClient.submitReindex(expectedUri)).thenReturn(expectedResponse); when(resourceDescriptionService.find(INSTANCE_RESOURCE)).thenReturn( Optional.of(resourceDescription(INSTANCE_RESOURCE))); - when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CONSORTIUM_TENANT_ID); + when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CENTRAL_TENANT_ID); var actual = indexService.reindexInventory(TENANT_ID, new ReindexRequest().recreateIndex(true)); diff --git a/src/test/java/org/folio/search/service/SearchTenantServiceTest.java b/src/test/java/org/folio/search/service/SearchTenantServiceTest.java index 45b28b390..8694a83e2 100644 --- a/src/test/java/org/folio/search/service/SearchTenantServiceTest.java +++ b/src/test/java/org/folio/search/service/SearchTenantServiceTest.java @@ -1,6 +1,6 @@ package org.folio.search.service; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.RESOURCE_NAME; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.resourceDescription; @@ -236,6 +236,6 @@ private TenantAttributes tenantAttributes() { } private Parameter centralTenantParameter() { - return new Parameter("centralTenantId").value(CONSORTIUM_TENANT_ID); + return new Parameter("centralTenantId").value(CENTRAL_TENANT_ID); } } diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiaServiceTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiaServiceTest.java index 57c7e5c4f..c8d22eac1 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiaServiceTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiaServiceTest.java @@ -1,7 +1,7 @@ package org.folio.search.service.consortium; import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.mockito.Mockito.when; @@ -29,7 +29,7 @@ class ConsortiaServiceTest { @Test void getCentralTenant_positive() { var userTenants = new UserTenantsClient.UserTenants(Collections.singletonList( - new UserTenantsClient.UserTenant(CONSORTIUM_TENANT_ID))); + new UserTenantsClient.UserTenant(CENTRAL_TENANT_ID))); when(userTenantsClient.getUserTenants(TENANT_ID)).thenReturn(userTenants); @@ -38,7 +38,7 @@ void getCentralTenant_positive() { assertThat(actual) .isNotEmpty() .get() - .isEqualTo(CONSORTIUM_TENANT_ID); + .isEqualTo(CENTRAL_TENANT_ID); } @Test diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java index 739087209..c94095d7c 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchHelperTest.java @@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.folio.search.utils.SearchUtils.CONTRIBUTOR_RESOURCE; import static org.folio.search.utils.SearchUtils.INSTANCE_SUBJECT_RESOURCE; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -53,11 +53,11 @@ class ConsortiumSearchHelperTest { void filterQueryForActiveAffiliation_positive_basic() { var query = matchAllQuery(); when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); consortiumSearchHelper.filterQueryForActiveAffiliation(query, "resource"); - verify(consortiumSearchHelper).filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID, "resource"); + verify(consortiumSearchHelper).filterQueryForActiveAffiliation(query, TENANT_ID, CENTRAL_TENANT_ID, "resource"); } @Test @@ -96,10 +96,10 @@ void filterQueryForActiveAffiliation_positive_noPrefix() { .should(termQuery("shared", true)); var actual = - consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID, "resource"); + consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, CENTRAL_TENANT_ID, "resource"); assertThat(actual).isEqualTo(expected); - verify(consortiumSearchHelper).filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID, "resource"); + verify(consortiumSearchHelper).filterQueryForActiveAffiliation(query, TENANT_ID, CENTRAL_TENANT_ID, "resource"); } @Test @@ -110,7 +110,7 @@ void filterQueryForActiveAffiliation_positive() { .should(termQuery(TENANT_ID_FIELD, TENANT_ID)) .should(termQuery(SHARED_FIELD, true)); - var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, CONSORTIUM_TENANT_ID, + var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, CENTRAL_TENANT_ID, CONTRIBUTOR_RESOURCE); assertThat(actual).isEqualTo(expected); @@ -125,7 +125,7 @@ void filterQueryForActiveAffiliation_positive_boolQuery() { .should(termQuery(SHARED_FIELD, true)); var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, - CONSORTIUM_TENANT_ID, INSTANCE_SUBJECT_RESOURCE); + CENTRAL_TENANT_ID, INSTANCE_SUBJECT_RESOURCE); assertThat(actual).isEqualTo(expected); } @@ -141,7 +141,7 @@ void filterQueryForActiveAffiliation_positive_boolQueryWithShould() { .should(termQuery(SHARED_FIELD, true))); var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, - CONSORTIUM_TENANT_ID, INSTANCE_SUBJECT_RESOURCE); + CENTRAL_TENANT_ID, INSTANCE_SUBJECT_RESOURCE); assertThat(actual).isEqualTo(expected); } @@ -156,7 +156,7 @@ void filterQueryForActiveAffiliation_positive_otherQuery() { .should(termQuery(SHARED_FIELD, true)); var actual = consortiumSearchHelper.filterQueryForActiveAffiliation(query, TENANT_ID, - CONSORTIUM_TENANT_ID, CONTRIBUTOR_RESOURCE); + CENTRAL_TENANT_ID, CONTRIBUTOR_RESOURCE); assertThat(actual).isEqualTo(expected); } @@ -201,7 +201,7 @@ void filterBrowseQueryForActiveAffiliation_positive_shared() { .must(termQuery(SHARED_FIELD, true)); when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query, "resource"); @@ -217,7 +217,7 @@ void filterBrowseQueryForActiveAffiliation_positive_local() { .must(termQuery(SHARED_FIELD, false)); when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query, "resource"); @@ -238,7 +238,7 @@ void filterBrowseQueryForActiveAffiliation_positive_localWithShould() { .must(termQuery(SHARED_FIELD, false)); when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var actual = consortiumSearchHelper.filterBrowseQueryForActiveAffiliation(browseContext, query, "resource"); @@ -265,7 +265,7 @@ void filterSubResourcesForConsortium_positive_notConsortiumTenant() { @Test void filterSubResourcesForConsortium_positive_memberTenant() { when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var browseContext = browseContext(null, "member"); var resource = new SubjectResource(); @@ -280,7 +280,7 @@ void filterSubResourcesForConsortium_positive_memberTenant() { @Test void filterSubResourcesForConsortium_positive_local() { when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var browseContext = browseContext(false, null); var subResources = subResources(); @@ -298,7 +298,7 @@ void filterSubResourcesForConsortium_positive_local() { @Test void filterSubResourcesForConsortium_positive_shared() { when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var browseContext = browseContext(true, null); var subResources = subResources(); @@ -341,8 +341,8 @@ private BrowseContext browseContext(Boolean sharedFilter, String tenantFilter) { private Set subResources() { return Set.of(subResource(TENANT_ID, false), subResource(TENANT_ID, true), - subResource(CONSORTIUM_TENANT_ID, false), - subResource(CONSORTIUM_TENANT_ID, true)); + subResource(CENTRAL_TENANT_ID, false), + subResource(CENTRAL_TENANT_ID, true)); } private InstanceSubResource subResource(String tenantId, boolean shared) { diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilderTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilderTest.java new file mode 100644 index 000000000..82f41bd64 --- /dev/null +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumSearchQueryBuilderTest.java @@ -0,0 +1,206 @@ +package org.folio.search.service.consortium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import org.apache.commons.lang3.StringUtils; +import org.folio.search.domain.dto.SortOrder; +import org.folio.search.model.Pair; +import org.folio.search.model.service.ConsortiumSearchContext; +import org.folio.search.model.types.ResourceType; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.FolioModuleMetadata; +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class ConsortiumSearchQueryBuilderTest { + + private @Mock FolioExecutionContext executionContext; + + @BeforeEach + void setUp() { + when(executionContext.getFolioModuleMetadata()).thenReturn(new FolioModuleMetadata() { + @Override + public String getModuleName() { + return "module"; + } + + @Override + public String getDBSchemaName(String tenantId) { + return "schema"; + } + }); + } + + @Test + void testBuildSelectQuery_forHoldingsResource_whenAllParametersDefined() { + var searchContext = new SearchContextMockBuilder().forHoldings().build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance WHERE instance_id = ? AND tenant_id = ?) i " + + "ORDER BY id desc LIMIT 100 OFFSET 10", actual); + } + + @NullAndEmptySource + @ParameterizedTest + void testBuildSelectQuery_forHoldingsResource_whenFiltersEmpty(String instanceId) { + var searchContext = new SearchContextMockBuilder().forHoldings() + .withInstanceId(instanceId).withTenantId(null).build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance ) i " + + "ORDER BY id desc LIMIT 100 OFFSET 10", actual); + } + + @NullAndEmptySource + @ParameterizedTest + void testBuildSelectQuery_forHoldingsResource_whenSortByEmpty(String sortBy) { + var searchContext = new SearchContextMockBuilder().forHoldings().withSortBy(sortBy).build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance WHERE instance_id = ? AND tenant_id = ?) i " + + "LIMIT 100 OFFSET 10", actual); + } + + @Test + void testBuildSelectQuery_forHoldingsResource_whenSortOrderEmpty() { + var searchContext = new SearchContextMockBuilder().forHoldings().withSortOrder(null).build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance WHERE instance_id = ? AND tenant_id = ?) i " + + "ORDER BY id LIMIT 100 OFFSET 10", actual); + } + + @Test + void testBuildSelectQuery_forHoldingsResource_whenLimitEmpty() { + var searchContext = new SearchContextMockBuilder().forHoldings().withLimit(null).build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance WHERE instance_id = ? AND tenant_id = ?) i " + + "ORDER BY id desc OFFSET 10", actual); + } + + @Test + void testBuildSelectQuery_forHoldingsResource_whenOffsetEmpty() { + var searchContext = new SearchContextMockBuilder().forHoldings().withOffset(null).build(); + + var actual = new ConsortiumSearchQueryBuilder(searchContext).buildSelectQuery(executionContext); + assertEquals("SELECT i.instance_id as instanceId, i.tenant_id as tenantId, i.holdings ->> 'id' AS id, " + + "i.holdings ->> 'hrid' AS hrid, i.holdings ->> 'callNumberPrefix' AS callNumberPrefix, " + + "i.holdings ->> 'callNumber' AS callNumber, i.holdings ->> 'copyNumber' AS copyNumber, " + + "i.holdings ->> 'permanentLocationId' AS permanentLocationId, " + + "i.holdings ->> 'discoverySuppress' AS discoverySuppress " + + "FROM (SELECT instance_id, tenant_id, json_array_elements(json -> 'holdings') as holdings " + + "FROM schema.consortium_instance WHERE instance_id = ? AND tenant_id = ?) i " + + "ORDER BY id desc LIMIT 100", actual); + } + + private static final class SearchContextMockBuilder { + private ResourceType resourceType; + private String instanceId = "inst123"; + private String tenantId = "tenant"; + private String sortBy = "id"; + private SortOrder sortOrder = SortOrder.DESC; + private Integer limit = 100; + private Integer offset = 10; + + SearchContextMockBuilder forHoldings() { + this.resourceType = ResourceType.HOLDINGS; + return this; + } + + SearchContextMockBuilder withInstanceId(String instanceId) { + this.instanceId = instanceId; + return this; + } + + SearchContextMockBuilder withTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + SearchContextMockBuilder withSortBy(String sortBy) { + this.sortBy = sortBy; + return this; + } + + SearchContextMockBuilder withSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + return this; + } + + SearchContextMockBuilder withLimit(Integer limit) { + this.limit = limit; + return this; + } + + SearchContextMockBuilder withOffset(Integer offset) { + this.offset = offset; + return this; + } + + ConsortiumSearchContext build() { + ConsortiumSearchContext searchContext = mock(ConsortiumSearchContext.class); + lenient().when(searchContext.getResourceType()).thenReturn(this.resourceType); + + lenient().when(searchContext.getFilters()).thenReturn(getFilters()); + lenient().when(searchContext.getSortBy()).thenReturn(this.sortBy); + lenient().when(searchContext.getSortOrder()).thenReturn(this.sortOrder); + lenient().when(searchContext.getLimit()).thenReturn(this.limit); + lenient().when(searchContext.getOffset()).thenReturn(this.offset); + return searchContext; + } + + private ArrayList> getFilters() { + var filters = new ArrayList>(); + if (StringUtils.isNotBlank(instanceId)) { + filters.add(Pair.pair("instanceId", instanceId)); + } + if (StringUtils.isNotBlank(tenantId)) { + filters.add(Pair.pair("tenantId", tenantId)); + } + return filters; + } + } +} diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumTenantExecutorTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumTenantExecutorTest.java index 681c8d01c..1c78513d5 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiumTenantExecutorTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumTenantExecutorTest.java @@ -1,7 +1,7 @@ package org.folio.search.service.consortium; import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -55,30 +55,30 @@ void execute_positive_consortiaMode() { var operation = Mockito.spy(operation()); when(folioExecutionContext.getTenantId()).thenReturn(TENANT_ID); - when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CONSORTIUM_TENANT_ID); + when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CENTRAL_TENANT_ID); doAnswer(invocationOnMock -> ((Callable) invocationOnMock.getArgument(1)).call()) - .when(scopedExecutionService).executeSystemUserScoped(eq(CONSORTIUM_TENANT_ID), any()); + .when(scopedExecutionService).executeSystemUserScoped(eq(CENTRAL_TENANT_ID), any()); var actual = consortiumTenantExecutor.execute(operation); assertThat(actual).isEqualTo(OPERATION_RESPONSE_MOCK); verify(operation).get(); - verify(scopedExecutionService).executeSystemUserScoped(eq(CONSORTIUM_TENANT_ID), any()); + verify(scopedExecutionService).executeSystemUserScoped(eq(CENTRAL_TENANT_ID), any()); } @Test void execute_positive_consortiaModeForTenant() { var operation = Mockito.spy(operation()); - when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CONSORTIUM_TENANT_ID); + when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CENTRAL_TENANT_ID); doAnswer(invocationOnMock -> ((Callable) invocationOnMock.getArgument(1)).call()) - .when(scopedExecutionService).executeSystemUserScoped(eq(CONSORTIUM_TENANT_ID), any()); + .when(scopedExecutionService).executeSystemUserScoped(eq(CENTRAL_TENANT_ID), any()); var actual = consortiumTenantExecutor.execute(TENANT_ID, operation); assertThat(actual).isEqualTo(OPERATION_RESPONSE_MOCK); verify(operation).get(); - verify(scopedExecutionService).executeSystemUserScoped(eq(CONSORTIUM_TENANT_ID), any()); + verify(scopedExecutionService).executeSystemUserScoped(eq(CENTRAL_TENANT_ID), any()); } @Test diff --git a/src/test/java/org/folio/search/service/consortium/ConsortiumTenantProviderTest.java b/src/test/java/org/folio/search/service/consortium/ConsortiumTenantProviderTest.java index cb51734bb..94025ec54 100644 --- a/src/test/java/org/folio/search/service/consortium/ConsortiumTenantProviderTest.java +++ b/src/test/java/org/folio/search/service/consortium/ConsortiumTenantProviderTest.java @@ -1,7 +1,7 @@ package org.folio.search.service.consortium; import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.mockito.Mockito.when; @@ -24,12 +24,12 @@ class ConsortiumTenantProviderTest { @Test void getTenant_positive() { - when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); var actual = consortiumTenantProvider.getTenant(TENANT_ID); assertThat(actual) - .isEqualTo(CONSORTIUM_TENANT_ID); + .isEqualTo(CENTRAL_TENANT_ID); } @Test diff --git a/src/test/java/org/folio/search/service/setter/authority/AuthoritySearchResponsePostProcessorTest.java b/src/test/java/org/folio/search/service/setter/authority/AuthoritySearchResponsePostProcessorTest.java index ed1533760..eab801242 100644 --- a/src/test/java/org/folio/search/service/setter/authority/AuthoritySearchResponsePostProcessorTest.java +++ b/src/test/java/org/folio/search/service/setter/authority/AuthoritySearchResponsePostProcessorTest.java @@ -2,7 +2,7 @@ import static org.apache.lucene.search.TotalHits.Relation.EQUAL_TO; import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +import static org.folio.search.utils.TestConstants.CENTRAL_TENANT_ID; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -74,10 +74,10 @@ private static Authority getAuthority(String id, AuthRefType reference) { @Test void shouldSetNumberOfTitles_whenProcessAuthorizedAuthoritiesThatHaveInstanceReferences() { when(context.getTenantId()).thenReturn(TENANT_ID); - when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CONSORTIUM_TENANT_ID); - when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(tenantProvider.getTenant(TENANT_ID)).thenReturn(CENTRAL_TENANT_ID); + when(consortiumTenantService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); when(searchFieldProvider.getFields("instance", "authorityId")).thenReturn(List.of("f1", "f2")); - mockSearchResponse(CONSORTIUM_TENANT_ID, 10, 11); + mockSearchResponse(CENTRAL_TENANT_ID, 10, 11); var authority1 = getAuthority("1", AuthRefType.AUTHORIZED); var authority2 = getAuthority("2", AuthRefType.AUTHORIZED); @@ -87,7 +87,7 @@ void shouldSetNumberOfTitles_whenProcessAuthorizedAuthoritiesThatHaveInstanceRef assertThat(authority2).extracting(Authority::getNumberOfTitles).isEqualTo(11); verify(searchRepository).msearch( - eq(SimpleResourceRequest.of("instance", CONSORTIUM_TENANT_ID)), searchSourceCaptor.capture()); + eq(SimpleResourceRequest.of("instance", CENTRAL_TENANT_ID)), searchSourceCaptor.capture()); var searchSources = searchSourceCaptor.getValue(); assertThat(searchSources) .hasSize(2) @@ -138,18 +138,18 @@ void shouldSetNumberOfTitles_whenNotInConsortium() { @Test void shouldSetNumberOfTitles_whenCentralTenant() { - when(context.getTenantId()).thenReturn(CONSORTIUM_TENANT_ID); - when(tenantProvider.getTenant(CONSORTIUM_TENANT_ID)).thenReturn(CONSORTIUM_TENANT_ID); - when(consortiumTenantService.getCentralTenant(CONSORTIUM_TENANT_ID)).thenReturn(Optional.of(CONSORTIUM_TENANT_ID)); + when(context.getTenantId()).thenReturn(CENTRAL_TENANT_ID); + when(tenantProvider.getTenant(CENTRAL_TENANT_ID)).thenReturn(CENTRAL_TENANT_ID); + when(consortiumTenantService.getCentralTenant(CENTRAL_TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); when(searchFieldProvider.getFields("instance", "authorityId")).thenReturn(List.of("f1", "f2")); - mockSearchResponse(CONSORTIUM_TENANT_ID, 10, 11); + mockSearchResponse(CENTRAL_TENANT_ID, 10, 11); var authority1 = getAuthority("1", AuthRefType.AUTHORIZED); var authority2 = getAuthority("2", AuthRefType.AUTHORIZED); processor.process(List.of(authority1, authority2)); verify(searchRepository).msearch( - eq(SimpleResourceRequest.of("instance", CONSORTIUM_TENANT_ID)), searchSourceCaptor.capture()); + eq(SimpleResourceRequest.of("instance", CENTRAL_TENANT_ID)), searchSourceCaptor.capture()); var searchSources = searchSourceCaptor.getValue(); assertThat(searchSources) .hasSize(2) @@ -157,7 +157,7 @@ void shouldSetNumberOfTitles_whenCentralTenant() { .map(query -> (BoolQueryBuilder) query) .allMatch(query -> query.should().size() == 2) .allMatch(query -> query.minimumShouldMatch().equals("1")) - .allMatch(query -> query.must().get(0).equals(affiliationQuery(CONSORTIUM_TENANT_ID, null))); + .allMatch(query -> query.must().get(0).equals(affiliationQuery(CENTRAL_TENANT_ID, null))); } private void mockSearchResponse(String tenantId, Integer... counts) { 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 cf55339b4..e9136460b 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -1,11 +1,14 @@ package org.folio.search.support.base; +import java.util.List; +import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import org.folio.cql2pgjson.model.CqlSort; import org.folio.search.domain.dto.BrowseOptionType; import org.folio.search.domain.dto.BrowseType; import org.folio.search.domain.dto.RecordType; import org.folio.search.domain.dto.TenantConfiguredFeature; +import org.folio.search.model.Pair; @UtilityClass public class ApiEndpoints { @@ -14,6 +17,18 @@ public static String instanceSearchPath() { return "/search/instances"; } + public static String consortiumHoldingsSearchPath() { + return "/search/consortium/holdings"; + } + + public static String consortiumHoldingsSearchPath(List> queryParams) { + var queryParamString = queryParams.stream() + .map(param -> param.getFirst() + "=" + param.getSecond()) + .collect(Collectors.joining("&")); + return consortiumHoldingsSearchPath() + "?" + queryParamString; + } + + public static String authoritySearchPath() { return "/search/authorities"; } 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 30b868ad4..74a2c8b1d 100644 --- a/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java +++ b/src/test/java/org/folio/search/support/base/BaseConsortiumIntegrationTest.java @@ -2,7 +2,7 @@ import static java.util.Arrays.asList; import static org.folio.search.support.base.ApiEndpoints.instanceSearchPath; -import static org.folio.search.utils.TestConstants.CONSORTIUM_TENANT_ID; +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.TENANT_ID; import static org.folio.search.utils.TestUtils.asJsonString; @@ -19,7 +19,6 @@ import org.folio.tenant.domain.dto.Parameter; import org.folio.tenant.domain.dto.TenantAttributes; import org.junit.jupiter.api.AfterAll; -import org.springframework.http.HttpHeaders; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -33,7 +32,7 @@ static void cleanUp() { @SneakyThrows protected static void removeTenant() { removeTenant(TENANT_ID); - removeTenant(CONSORTIUM_TENANT_ID); + removeTenant(CENTRAL_TENANT_ID); } @SneakyThrows @@ -59,7 +58,7 @@ private static void setUpTenant(String tenant, String validationPath, Runnab @SneakyThrows protected static void enableTenant(String tenant) { var tenantAttributes = new TenantAttributes().moduleTo("mod-search"); - tenantAttributes.addParametersItem(new Parameter("centralTenantId").value(CONSORTIUM_TENANT_ID)); + tenantAttributes.addParametersItem(new Parameter("centralTenantId").value(CENTRAL_TENANT_ID)); mockMvc.perform(post("/_/tenant", randomId()) .content(asJsonString(tenantAttributes)) @@ -69,17 +68,37 @@ protected static void enableTenant(String tenant) { } @SneakyThrows - public static ResultActions doGet(String uri, Object... args) { + public static ResultActions tryGet(String uri, Object... args) { + return tryGet(uri, MEMBER_TENANT_ID, args); + } + + @SneakyThrows + public static ResultActions tryGet(String uri, String tenantHeader, Object... args) { return mockMvc.perform(get(uri, args) - .headers(defaultHeaders()) - .accept("application/json;charset=UTF-8")) + .headers(defaultHeaders(tenantHeader)) + .accept("application/json;charset=UTF-8")); + } + + @SneakyThrows + public static ResultActions doGet(String uri, Object... args) { + return doGet(uri, MEMBER_TENANT_ID, args); + } + + @SneakyThrows + public static ResultActions doGet(String uri, String tenantHeader, Object... args) { + return tryGet(uri, tenantHeader, args) .andExpect(status().isOk()); } @SneakyThrows public static ResultActions doGet(MockHttpServletRequestBuilder request) { + return doGet(request, MEMBER_TENANT_ID); + } + + @SneakyThrows + public static ResultActions doGet(MockHttpServletRequestBuilder request, String tenantHeader) { return mockMvc.perform(request - .headers(defaultHeaders()) + .headers(defaultHeaders(tenantHeader)) .accept("application/json;charset=UTF-8")) .andExpect(status().isOk()); } @@ -94,7 +113,4 @@ protected static ResultActions doSearchByInstances(String query, boolean expandA return doSearch(instanceSearchPath(), MEMBER_TENANT_ID, query, null, null, expandAll); } - public static HttpHeaders defaultHeaders() { - return defaultHeaders(MEMBER_TENANT_ID); - } } diff --git a/src/test/java/org/folio/search/utils/TestConstants.java b/src/test/java/org/folio/search/utils/TestConstants.java index e2fdae8fd..83a0526a1 100644 --- a/src/test/java/org/folio/search/utils/TestConstants.java +++ b/src/test/java/org/folio/search/utils/TestConstants.java @@ -18,7 +18,7 @@ public class TestConstants { public static final String ENV = "folio"; public static final String TENANT_ID = "test_tenant"; public static final String MEMBER_TENANT_ID = "member_tenant"; - public static final String CONSORTIUM_TENANT_ID = "consortium"; + public static final String CENTRAL_TENANT_ID = "consortium"; public static final String RESOURCE_ID = "d148dd82-56b0-4ddb-a638-83ca1ee97e0a"; public static final String RESOURCE_ID_SECOND = "d148dd82-56b0-4ddb-a638-83ca1ee97e0b"; public static final String EMPTY_OBJECT = "{}"; diff --git a/src/test/resources/samples/instance-response-sample/instance-basic-response.json b/src/test/resources/samples/instance-response-sample/instance-basic-response.json new file mode 100644 index 000000000..f63a7e3cb --- /dev/null +++ b/src/test/resources/samples/instance-response-sample/instance-basic-response.json @@ -0,0 +1,67 @@ +{ + "totalRecords": 1, + "instances": [ + { + "id": "5bf370e0-8cca-4d9c-82e4-5170ab2a0a39", + "tenantId": "test_tenant", + "shared": false, + "title": "A semantic web primer :0747-0850 & wolves", + "contributors": [ + { + "name": "Antoniou, Grigoris matthew", + "contributorTypeId": "6e09d47d-95e2-4d8a-831b-f777b8ef6d81", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a" + }, + { + "name": "Van Harmelen, Frank", + "contributorTypeId": "6e09d47d-95e2-4d8a-831b-f777b8ef6d81", + "contributorNameTypeId": "9fb7f83e-260e-479f-9539-dfd9a628b858", + "authorityId": "55294032-fcf6-45cc-b6da-4420a61ef72c", + "primary": true + } + ], + "publication": [ + { + "publisher": "MIT Press", + "dateOfPublication": "c2004" + } + ], + "staffSuppress": false, + "discoverySuppress": false, + "isBoundWith": true, + "electronicAccess": [], + "notes": [], + "items": [ + { + "chronology": "", + "copyNumber": "Copy 2", + "effectiveCallNumberComponents": { + "callNumber": "TK5105.88815 . A58 2004 FT MEADE", + "prefix": "prefix-10101", + "suffix": "suffix-10101", + "typeId": "512173a7-bd09-490e-b773-17d83f2b63fe" + }, + "effectiveShelvingOrder": "TK 45105.88815 A58 42004 FT MEADE", + "enumeration": "", + "notes": [], + "volume": "v1", + "yearCaption": [] + }, + { + "chronology": "", + "effectiveCallNumberComponents": { + "callNumber": "TK5105.88815 . A58 2004 FT MEADE", + "prefix": "prefix-90000", + "suffix": "suffix-90000", + "typeId": "512173a7-bd09-490e-b773-17d83f2b63fe" + }, + "effectiveShelvingOrder": "TK 45105.88815 A58 42004 FT MEADE", + "enumeration": "", + "notes": [], + "yearCaption": [] + } + ], + "holdings": [] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/samples/instance-response-sample/instance-full-response.json b/src/test/resources/samples/instance-response-sample/instance-full-response.json new file mode 100644 index 000000000..c1fdbaf2f --- /dev/null +++ b/src/test/resources/samples/instance-response-sample/instance-full-response.json @@ -0,0 +1,383 @@ +{ + "totalRecords": 1, + "instances": [ + { + "id": "5bf370e0-8cca-4d9c-82e4-5170ab2a0a39", + "tenantId": "test_tenant", + "shared": false, + "hrid": "inst000000000022", + "source": "FOLIO", + "statisticalCodeIds": [ + "b5968c9e-cddc-4576-99e3-8e60aed8b0dd" + ], + "statusId": "9634a5ab-9228-4703-baf2-4d12ebc77d56", + "title": "A semantic web primer :0747-0850 & wolves", + "indexTitle": "Semantic web primer", + "series": [ + { + "value": "Cooperative information systems", + "authorityId": "9d968396-0cce-4e9f-8867-c4d04c01f535" + } + ], + "alternativeTitles": [ + { + "alternativeTitle": "An alternative title" + }, + { + "alternativeTitle": "déjà vu" + }, + { + "alternativeTitle": "Le parler arabe de Cherchell, Algérie." + }, + { + "alternativeTitleTypeId": "9d968396-0cce-4e9f-8867-c4d04c01f535", + "alternativeTitle": "An Uniform title" + }, + { + "alternativeTitle": "Pang'ok bangk'asyurangsŭ", + "authorityId": "9d968396-0cce-4e9f-8867-c4d04c01f535" + } + ], + "identifiers": [ + { + "value": "0262012103", + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422" + }, + { + "value": "9781609383657", + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422" + }, + { + "value": "9780262012102", + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422" + }, + { + "value": "978-0-471-44250-9 (cloth : alk. paper)", + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422" + }, + { + "value": "047144250X", + "identifierTypeId": "8261054f-be78-422d-bd51-4ed9f33c3422" + }, + { + "value": "2003065165", + "identifierTypeId": "c858e4f2-2b6b-4385-842b-60732ee14abb" + }, + { + "value": "n 2003075732", + "identifierTypeId": "c858e4f2-2b6b-4385-842b-60732ee14abb" + }, + { + "value": "0317-8471", + "identifierTypeId": "913300b2-03ed-469a-8179-c1092c991227" + }, + { + "value": "(OCoLC)ocm0060710867", + "identifierTypeId": "c3c651c7-96b4-416c-a1af-17146ce0a409" + }, + { + "value": "ocm0012345 800630", + "identifierTypeId": "82fb97e1-f460-4099-9ac8-97518341ed1a" + } + ], + "contributors": [ + { + "name": "Antoniou, Grigoris matthew", + "contributorTypeId": "6e09d47d-95e2-4d8a-831b-f777b8ef6d81", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a" + }, + { + "name": "Van Harmelen, Frank", + "contributorTypeId": "6e09d47d-95e2-4d8a-831b-f777b8ef6d81", + "contributorNameTypeId": "9fb7f83e-260e-479f-9539-dfd9a628b858", + "authorityId": "55294032-fcf6-45cc-b6da-4420a61ef72c", + "primary": true + } + ], + "subjects": [ + { + "value": "Semantic Web", + "authorityId": "55294032-fcf6-45cc-b6da-4420a61ef72d" + } + ], + "instanceTypeId": "6312d172-f0cf-40f6-b27d-9fa8feaf332f", + "instanceFormatIds": [ + "7f9c4ac0-fa3d-43b7-b978-3bf0be38c4da" + ], + "languages": [ + "eng" + ], + "metadata": { + "createdDate": "2020-12-08T15:47:13.625+00:00", + "updatedDate": "2021-01-15T15:17:22.851+00:00", + "updatedByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925" + }, + "administrativeNotes": [ + "original + pcc", + "for deletion" + ], + "modeOfIssuanceId": "9d18a02f-5897-4c31-9106-c9abb5c7ae8b", + "natureOfContentTermIds": [ + "85657646-6b6f-4e71-b54c-d47f3b95a5ed" + ], + "publication": [ + { + "publisher": "MIT Press", + "dateOfPublication": "c2004" + } + ], + "staffSuppress": false, + "discoverySuppress": false, + "isBoundWith": true, + "tags": { + "tagList": [ + "book", + "electronic", + "electronic book" + ] + }, + "classifications": [ + { + "classificationNumber": "025.04" + }, + { + "classificationNumber": "TK5105.88815 .A58 2004" + } + ], + "electronicAccess": [ + { + "uri": "https://testlibrary.sample.com/journal/10.1002/(ISSN)1938-3703", + "linkText": "electronic access link text", + "publicNote": "Online access via Wiley (1996-current)" + } + ], + "notes": [ + { + "note": "Includes bibliographical references and index.", + "staffOnly": false + }, + { + "note": "The development of the Semantic Web, with machine-readable content, has the potential to revolutionize the World Wide Web and its uses. A Semantic Web Primer provides an introduction and guide to this continuously evolving field, describing its key ideas, languages, and technologies. Suitable for use as a textbook or for independent study by professionals, it concentrates on undergraduate-level fundamental concepts and techniques that will enable readers to proceed with building applications on their own and includes exercises, project descriptions, and annotated references to relevant online materials. The third edition of this widely used text has been thoroughly updated, with significant new material that reflects a rapidly developing field. Treatment of the different languages (OWL2, rules) expands the coverage of RDF and OWL, defining the data model independently of XML and including coverage of N3/Turtle and RDFa. A chapter is devoted to OWL2, the new W3C standard. This edition also features additional coverage of the query language SPARQL, the rule language RIF and the possibility of interaction between rules and ontology languages and applications. The chapter on Semantic Web applications reflects the rapid developments of the past few years. A new chapter offers ideas for term projects", + "staffOnly": false + }, + { + "note": "Librarian private note", + "staffOnly": true + } + ], + "items": [ + { + "accessionNumber": "item_accession_number", + "administrativeNotes": [ + "v1.1", + "need attention" + ], + "barcode": "10101", + "chronology": "", + "circulationNotes": [ + { + "note": "testCirculationNote", + "staffOnly": false + } + ], + "copyNumber": "Copy 2", + "discoverySuppress": false, + "effectiveCallNumberComponents": { + "callNumber": "TK5105.88815 . A58 2004 FT MEADE", + "prefix": "prefix-10101", + "suffix": "suffix-10101", + "typeId": "512173a7-bd09-490e-b773-17d83f2b63fe" + }, + "effectiveLocationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "effectiveShelvingOrder": "TK 45105.88815 A58 42004 FT MEADE", + "electronicAccess": [ + { + "uri": "https://www.loc.gov/catdir/toc/ecip0718/2007020429.html", + "linkText": "Links available", + "publicNote": "Table of contents only" + } + ], + "enumeration": "", + "formerIds": [ + "207b9372-127f-4bdd-83e0-147c9fe9bc16", + "81ae0f60-f2bc-450c-84c8-5a21096daed9" + ], + "hrid": "item000000000014", + "id": "7212ba6a-8dcf-45a1-be9a-ffaa847c4423", + "itemIdentifier": "itemIdentifierFieldValue", + "itemLevelCallNumberTypeId": "5ba6b62e-6858-490a-8102-5b1369873835", + "materialTypeId": "1a54b431-2e4f-452d-9cae-9cee66c9a892", + "metadata": { + "createdDate": "2020-12-08T15:47:21.327+00:00", + "updatedDate": "2021-01-18T08:17:56.752+00:00", + "updatedByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925" + }, + "notes": [], + "statisticalCodeIds": [ + "b5968c9e-cddc-4576-99e3-8e60aed8b0dd" + ], + "status": { + "name": "Available" + }, + "tags": { + "tagList": [ + "item-tag" + ] + }, + "volume": "v1", + "yearCaption": [] + }, + { + "administrativeNotes": [ + "v1.2" + ], + "barcode": "90000", + "chronology": "", + "circulationNotes": [ + { + "note": "private circulation note", + "staffOnly": true + }, + { + "note": "public circulation note", + "staffOnly": false + } + ], + "discoverySuppress": false, + "effectiveCallNumberComponents": { + "callNumber": "TK5105.88815 . A58 2004 FT MEADE", + "prefix": "prefix-90000", + "suffix": "suffix-90000", + "typeId": "512173a7-bd09-490e-b773-17d83f2b63fe" + }, + "effectiveLocationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "effectiveShelvingOrder": "TK 45105.88815 A58 42004 FT MEADE", + "electronicAccess": [ + { + "uri": "http://www.loc.gov/catdir/toc/ecip0718/2007020429.html", + "linkText": "Links available", + "publicNote": "Table of contents only" + } + ], + "enumeration": "", + "formerIds": [], + "hrid": "item000000000015", + "id": "100d10bf-2f06-4aa0-be15-0b95b2d9f9e3", + "materialTypeId": "1a54b431-2e4f-452d-9cae-9cee66c9a892", + "metadata": { + "createdDate": "2020-12-08T15:47:22.827+00:00", + "updatedDate": "2020-12-08T15:47:22.827+00:00" + }, + "notes": [ + { + "note": "Includes bibliographical references and index of item.", + "staffOnly": false + }, + { + "note": "Librarian public note for item", + "staffOnly": false + }, + { + "note": "Librarian private note for item", + "staffOnly": true + } + ], + "statisticalCodeIds": [ + "615e9911-edb1-4ab3-a9c3-a461a3de02f8" + ], + "status": { + "name": "Available" + }, + "yearCaption": [] + } + ], + "holdings": [ + { + "administrativeNotes": [ + "v2.0", + "for deletion" + ], + "callNumber": null, + "discoverySuppress": true, + "electronicAccess": [], + "formerIds": [ + "1d76ee84-d776-48d2-ab96-140c24e39ac5" + ], + "holdingsTypeId": "03c9c400-b9e3-4a07-ac0e-05ab470233ed", + "hrid": "hold000000000009", + "id": "e3ff6133-b9a2-4d4c-a1c9-dc1867d4df19", + "metadata": { + "createdDate": "2020-12-08T15:47:19.330+00:00", + "updatedDate": "2020-12-08T15:47:19.330+00:00" + }, + "notes": [], + "permanentLocationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "statisticalCodeIds": [ + "a2b01891-c9ab-4d04-8af8-8989af1c6aad" + ] + }, + { + "discoverySuppress": false, + "electronicAccess": [ + { + "uri": "https://testlibrary.sample.com/holdings?hrid=ho0000006", + "linkText": "Holding's electronicAccess link Text", + "publicNote": "Holding's electronicAccess public note" + } + ], + "formerIds": [ + "ac727d8c-f422-4c3f-815e-e85694095e71", + "9b8ec096-fa2e-451b-8e7a-6d1c977ee946" + ], + "hrid": "ho00000000006", + "id": "9550c935-401a-4a85-875e-4d1fe7678870", + "metadata": { + "createdDate": "2021-03-04T12:36:17.211+00:00", + "createdByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925", + "updatedDate": "2021-03-04T12:36:17.211+00:00", + "updatedByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925" + }, + "notes": [ + { + "note": "Includes bibliographical references and index of holdings.", + "staffOnly": false + }, + { + "note": "Librarian public note for holding", + "staffOnly": false + }, + { + "note": "Librarian private note for holding", + "staffOnly": true + } + ], + "permanentLocationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "sourceId": "f32d531e-df79-46b3-8932-cdd35f7a2264", + "statisticalCodeIds": [] + }, + { + "discoverySuppress": false, + "electronicAccess": [], + "formerIds": [], + "hrid": "ho00000000007", + "id": "a663dea9-6547-4b2d-9daa-76cadd662272", + "metadata": { + "createdDate": "2021-03-04T12:36:57.751+00:00", + "createdByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925", + "updatedDate": "2021-03-04T12:36:57.751+00:00", + "updatedByUserId": "d2533d6c-a42c-5bff-ad77-22f87c297925" + }, + "notes": [], + "permanentLocationId": "758258bc-ecc1-41b8-abca-f7b610822ffd", + "sourceId": "f32d531e-df79-46b3-8932-cdd35f7a2264", + "statisticalCodeIds": [], + "tags": { + "tagList": [ + "holdings-tag" + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/samples/semantic-web-primer/holdings.json b/src/test/resources/samples/semantic-web-primer/holdings.json index 7ff172b68..60ffef1e4 100644 --- a/src/test/resources/samples/semantic-web-primer/holdings.json +++ b/src/test/resources/samples/semantic-web-primer/holdings.json @@ -2,6 +2,7 @@ { "id": "e3ff6133-b9a2-4d4c-a1c9-dc1867d4df19", "hrid": "hold000000000009", + "discoverySuppress": true, "holdingsTypeId": "03c9c400-b9e3-4a07-ac0e-05ab470233ed", "formerIds": [ "1d76ee84-d776-48d2-ab96-140c24e39ac5"