diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 88d904ede..40f337018 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -476,7 +476,8 @@ ], "modulePermissions": [ "user-tenants.collection.get", - "inventory-storage.classification-types.collection.get" + "inventory-storage.classification-types.collection.get", + "inventory-storage.call-number-types.collection.get" ] } ] diff --git a/src/main/java/org/folio/search/integration/message/KafkaMessageListener.java b/src/main/java/org/folio/search/integration/message/KafkaMessageListener.java index 4e0bfbf28..7ff89c605 100644 --- a/src/main/java/org/folio/search/integration/message/KafkaMessageListener.java +++ b/src/main/java/org/folio/search/integration/message/KafkaMessageListener.java @@ -118,23 +118,28 @@ public void handleAuthorityEvents(List> co } @KafkaListener( - id = KafkaConstants.CLASSIFICATION_TYPE_LISTENER_ID, + id = KafkaConstants.BROWSE_CONFIG_DATA_LISTENER_ID, containerFactory = "resourceListenerContainerFactory", - groupId = "#{folioKafkaProperties.listener['classification-type'].groupId}", - concurrency = "#{folioKafkaProperties.listener['classification-type'].concurrency}", - topicPattern = "#{folioKafkaProperties.listener['classification-type'].topicPattern}") + groupId = "#{folioKafkaProperties.listener['browse-config-data'].groupId}", + concurrency = "#{folioKafkaProperties.listener['browse-config-data'].concurrency}", + topicPattern = "#{folioKafkaProperties.listener['browse-config-data'].topicPattern}") @CacheEvict(cacheNames = REFERENCE_DATA_CACHE, allEntries = true) - public void handleClassificationTypeEvents(List> consumerRecords) { - log.info("Processing classification-type events from Kafka [number of events: {}]", consumerRecords.size()); + public void handleBrowseConfigDataEvents(List> consumerRecords) { + log.info("Processing browse config data events from Kafka [number of events: {}]", consumerRecords.size()); var batch = consumerRecords.stream() .map(ConsumerRecord::value) - .filter(resourceEvent -> resourceEvent.getType() == DELETE).toList(); + .filter(resourceEvent -> resourceEvent.getType() == DELETE) + .toList(); var batchByTenant = batch.stream().collect(Collectors.groupingBy(ResourceEvent::getTenant)); batchByTenant.forEach((tenant, resourceEvents) -> executionService.executeSystemUserScoped(tenant, () -> { folioMessageBatchProcessor.consumeBatchWithFallback(batch, KAFKA_RETRY_TEMPLATE_NAME, - resourceEvent -> configSynchronizationService.sync(resourceEvent, ResourceType.CLASSIFICATION_TYPE), + resourceEvent -> { + var eventsByResource = resourceEvent.stream().collect(Collectors.groupingBy(ResourceEvent::getResourceName)); + eventsByResource.forEach((resourceName, events) -> + configSynchronizationService.sync(resourceEvent, ResourceType.byName(resourceName))); + }, KafkaMessageListener::logFailedEvent); return null; })); diff --git a/src/main/java/org/folio/search/integration/message/interceptor/ResourceEventBatchInterceptor.java b/src/main/java/org/folio/search/integration/message/interceptor/ResourceEventBatchInterceptor.java index 3f69275af..355dba69d 100644 --- a/src/main/java/org/folio/search/integration/message/interceptor/ResourceEventBatchInterceptor.java +++ b/src/main/java/org/folio/search/integration/message/interceptor/ResourceEventBatchInterceptor.java @@ -27,6 +27,7 @@ public class ResourceEventBatchInterceptor implements BatchInterceptor resourceEvent, ResourceType resourceType) { if (resourceEvent == null || resourceEvent.isEmpty()) { return; } - if (resourceType == ResourceType.CLASSIFICATION_TYPE) { - var ids = resourceEvent.stream().map(ResourceEvent::getId).toList(); - configService.deleteTypeIdsFromConfigs(BrowseType.INSTANCE_CLASSIFICATION, ids); - } + + Optional.ofNullable(resourceType) + .map(resource -> switch (resourceType) { + case CLASSIFICATION_TYPE -> BrowseType.CLASSIFICATION; + case CALL_NUMBER_TYPE -> BrowseType.CALL_NUMBER; + default -> null; }) + .ifPresentOrElse(browseType -> { + var ids = resourceEvent.stream().map(ResourceEvent::getId).toList(); + configService.deleteTypeIdsFromConfigs(browseType, ids); + }, () -> log.warn("sync:: not supported resource type: [{}]", resourceType)); } } diff --git a/src/main/java/org/folio/search/utils/KafkaConstants.java b/src/main/java/org/folio/search/utils/KafkaConstants.java index dcded2a04..224b22cb3 100644 --- a/src/main/java/org/folio/search/utils/KafkaConstants.java +++ b/src/main/java/org/folio/search/utils/KafkaConstants.java @@ -7,7 +7,7 @@ public class KafkaConstants { public static final String AUTHORITY_LISTENER_ID = "mod-search-authorities-listener"; public static final String EVENT_LISTENER_ID = "mod-search-events-listener"; - public static final String CLASSIFICATION_TYPE_LISTENER_ID = "mod-search-classification-type-listener"; + public static final String BROWSE_CONFIG_DATA_LISTENER_ID = "mod-search-browse-config-data-listener"; public static final String LOCATION_LISTENER_ID = "mod-search-location-listener"; public static final String LINKED_DATA_LISTENER_ID = "mod-search-linked-data-listener"; public static final String REINDEX_RANGE_INDEX_LISTENER_ID = "mod-search-reindex-index-listener"; diff --git a/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java index db4d26137..1117d4f0e 100644 --- a/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java +++ b/src/main/java/org/folio/search/utils/ShelvingOrderCalculationHelper.java @@ -3,9 +3,11 @@ import java.util.Locale; import lombok.NonNull; import lombok.experimental.UtilityClass; +import org.folio.search.cql.SuDocCallNumber; import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.marc4j.callnum.DeweyCallNumber; import org.marc4j.callnum.LCCallNumber; +import org.marc4j.callnum.NlmCallNumber; @UtilityClass public class ShelvingOrderCalculationHelper { @@ -14,6 +16,8 @@ public static String calculate(@NonNull String input, @NonNull ShelvingOrderAlgo return switch (algorithmType) { case LC -> new LCCallNumber(input).getShelfKey().trim(); case DEWEY -> new DeweyCallNumber(input).getShelfKey().trim(); + case NLM -> new NlmCallNumber(input).getShelfKey().trim(); + case SUDOC -> new SuDocCallNumber(input).getShelfKey().trim(); case DEFAULT -> normalize(input); }; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 700a4aeee..0f7d2962a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -126,10 +126,10 @@ folio: concurrency: ${KAFKA_AUTHORITIES_CONCURRENCY:1} topic-pattern: ${KAFKA_AUTHORITIES_CONSUMER_PATTERN:(${folio.environment}\.)(.*\.)authorities\.authority} group-id: ${folio.environment}-mod-search-authorities-group - classification-type: - concurrency: ${KAFKA_CLASSIFICATION_TYPE_CONCURRENCY:1} - topic-pattern: (${folio.environment}\.)(.*\.)inventory\.classification-type - group-id: ${folio.environment}-mod-search-classification-type-group + browse-config-data: + concurrency: ${KAFKA_BROWSE_CONFIG_DATA_CONCURRENCY:1} + topic-pattern: (${folio.environment}\.)(.*\.)inventory\.(classification-type|call-number-type) + group-id: ${folio.environment}-mod-search-browse-config-data-group location: concurrency: ${KAFKA_LOCATION_CONCURRENCY:1} topic-pattern: (${folio.environment}\.)(.*\.)inventory\.(location|campus|institution|library) diff --git a/src/main/resources/changelog/changelog-master.xml b/src/main/resources/changelog/changelog-master.xml index a12c27aa2..4e3511bdd 100644 --- a/src/main/resources/changelog/changelog-master.xml +++ b/src/main/resources/changelog/changelog-master.xml @@ -11,4 +11,5 @@ + diff --git a/src/main/resources/changelog/changes/v4.1/populate-call-number-browse-config.xml b/src/main/resources/changelog/changes/v4.1/populate-call-number-browse-config.xml new file mode 100644 index 000000000..c621c8b43 --- /dev/null +++ b/src/main/resources/changelog/changes/v4.1/populate-call-number-browse-config.xml @@ -0,0 +1,48 @@ + + + + + + + + + Populate browse_config table with default call number values + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/swagger.api/schemas/entity/browseOptionType.yaml b/src/main/resources/swagger.api/schemas/entity/browseOptionType.yaml index 11456a05a..83e0b0105 100644 --- a/src/main/resources/swagger.api/schemas/entity/browseOptionType.yaml +++ b/src/main/resources/swagger.api/schemas/entity/browseOptionType.yaml @@ -4,3 +4,6 @@ enum: - all - lc - dewey + - nlm + - sudoc + - other diff --git a/src/main/resources/swagger.api/schemas/entity/browseType.yaml b/src/main/resources/swagger.api/schemas/entity/browseType.yaml index 0b6be8357..f31c77c43 100644 --- a/src/main/resources/swagger.api/schemas/entity/browseType.yaml +++ b/src/main/resources/swagger.api/schemas/entity/browseType.yaml @@ -1,3 +1,4 @@ type: string enum: - instance-classification + - instance-call-number diff --git a/src/main/resources/swagger.api/schemas/entity/shelvingOrderAlgorithmType.yaml b/src/main/resources/swagger.api/schemas/entity/shelvingOrderAlgorithmType.yaml index ea57fb14c..951db6e95 100644 --- a/src/main/resources/swagger.api/schemas/entity/shelvingOrderAlgorithmType.yaml +++ b/src/main/resources/swagger.api/schemas/entity/shelvingOrderAlgorithmType.yaml @@ -3,4 +3,6 @@ type: string enum: - lc - dewey + - nlm + - sudoc - default diff --git a/src/test/java/org/folio/search/controller/BrowseClassificationIT.java b/src/test/java/org/folio/search/controller/BrowseClassificationIT.java index 991441011..b33a11b29 100644 --- a/src/test/java/org/folio/search/controller/BrowseClassificationIT.java +++ b/src/test/java/org/folio/search/controller/BrowseClassificationIT.java @@ -154,7 +154,7 @@ private static void updateLcConfig(List typeIds) { .typeIds(typeIds); var stub = mockClassificationTypes(okapi.wireMockServer(), typeIds.toArray(new UUID[0])); - doPut(browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION, BrowseOptionType.LC), config); + doPut(browseConfigPath(BrowseType.CLASSIFICATION, BrowseOptionType.LC), config); okapi.wireMockServer().removeStub(stub); } diff --git a/src/test/java/org/folio/search/controller/ConfigControllerTest.java b/src/test/java/org/folio/search/controller/ConfigControllerTest.java index 1791a2988..c11d33d47 100644 --- a/src/test/java/org/folio/search/controller/ConfigControllerTest.java +++ b/src/test/java/org/folio/search/controller/ConfigControllerTest.java @@ -1,7 +1,6 @@ package org.folio.search.controller; import static java.util.UUID.randomUUID; -import static org.folio.search.domain.dto.BrowseType.INSTANCE_CLASSIFICATION; import static org.folio.search.domain.dto.TenantConfiguredFeature.SEARCH_ALL_FIELDS; import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestUtils.asJsonString; @@ -26,6 +25,7 @@ import org.folio.search.domain.dto.BrowseConfig; import org.folio.search.domain.dto.BrowseConfigCollection; import org.folio.search.domain.dto.BrowseOptionType; +import org.folio.search.domain.dto.BrowseType; import org.folio.search.domain.dto.FeatureConfig; import org.folio.search.domain.dto.FeatureConfigs; import org.folio.search.domain.dto.LanguageConfigs; @@ -40,6 +40,8 @@ import org.folio.spring.testing.type.UnitTest; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -277,14 +279,15 @@ void deleteFeatureConfigurationById_positive() throws Exception { mockMvc.perform(request).andExpect(status().isNoContent()); } - @Test - void getBrowseConfigs_positive() throws Exception { + @ParameterizedTest + @EnumSource(BrowseType.class) + void getBrowseConfigs_positive(BrowseType type) throws Exception { var config = new BrowseConfig().id(BrowseOptionType.LC).shelvingAlgorithm(ShelvingOrderAlgorithmType.LC) .typeIds(List.of(randomUUID(), randomUUID())); - when(browseConfigService.getConfigs(INSTANCE_CLASSIFICATION)) + when(browseConfigService.getConfigs(type)) .thenReturn(new BrowseConfigCollection().addConfigsItem(config).totalRecords(1)); - var request = get(ApiEndpoints.browseConfigPath(INSTANCE_CLASSIFICATION)) + var request = get(ApiEndpoints.browseConfigPath(type)) .header(XOkapiHeaders.TENANT, TENANT_ID) .contentType(APPLICATION_JSON); @@ -297,13 +300,14 @@ void getBrowseConfigs_positive() throws Exception { containsInAnyOrder(config.getTypeIds().stream().map(UUID::toString).toArray()))); } - @Test - void putBrowseConfig_positive() throws Exception { + @ParameterizedTest + @EnumSource(BrowseType.class) + void putBrowseConfig_positive(BrowseType type) throws Exception { var config = new BrowseConfig().id(BrowseOptionType.LC).shelvingAlgorithm(ShelvingOrderAlgorithmType.LC) .typeIds(List.of(randomUUID(), randomUUID())); - doNothing().when(browseConfigService).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + doNothing().when(browseConfigService).upsertConfig(type, BrowseOptionType.LC, config); - var request = put(ApiEndpoints.browseConfigPath(INSTANCE_CLASSIFICATION, BrowseOptionType.LC)) + var request = put(ApiEndpoints.browseConfigPath(type, BrowseOptionType.LC)) .header(XOkapiHeaders.TENANT, TENANT_ID) .contentType(APPLICATION_JSON) .content(asJsonString(config)); diff --git a/src/test/java/org/folio/search/controller/ConfigIT.java b/src/test/java/org/folio/search/controller/ConfigIT.java index 61b093953..930cb3ec5 100644 --- a/src/test/java/org/folio/search/controller/ConfigIT.java +++ b/src/test/java/org/folio/search/controller/ConfigIT.java @@ -11,9 +11,14 @@ import static org.folio.search.utils.SearchConverterUtils.getMapValueByPath; import static org.folio.search.utils.SearchUtils.ID_FIELD; import static org.folio.search.utils.SearchUtils.getIndexName; +import static org.folio.search.utils.TestConstants.INVENTORY_CALL_NUMBER_TYPE_TOPIC; +import static org.folio.search.utils.TestConstants.INVENTORY_CLASSIFICATION_TYPE_TOPIC; import static org.folio.search.utils.TestConstants.TENANT_ID; +import static org.folio.search.utils.TestConstants.getTopicName; +import static org.folio.search.utils.TestConstants.inventoryCallNumberTopic; import static org.folio.search.utils.TestConstants.inventoryClassificationTopic; import static org.folio.search.utils.TestUtils.mapOf; +import static org.folio.search.utils.TestUtils.mockCallNumberTypes; import static org.folio.search.utils.TestUtils.mockClassificationTypes; import static org.folio.search.utils.TestUtils.parseResponse; import static org.folio.search.utils.TestUtils.randomId; @@ -49,6 +54,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.opensearch.action.search.SearchRequest; import org.opensearch.client.RequestOptions; import org.opensearch.client.RestHighLevelClient; @@ -230,13 +237,19 @@ void deleteUnknownFeature_notExists() throws Exception { } @Test - void getBrowseConfigs_positive() throws Exception { - doGet(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION)) + void getBrowseConfigs_positive_classification() throws Exception { + doGet(ApiEndpoints.browseConfigPath(BrowseType.CLASSIFICATION)) .andExpect(jsonPath("$.totalRecords", is(3))); } @Test - void putBrowseConfigs_positive() throws Exception { + void getBrowseConfigs_positive_callNumber() throws Exception { + doGet(ApiEndpoints.browseConfigPath(BrowseType.CALL_NUMBER)) + .andExpect(jsonPath("$.totalRecords", is(6))); + } + + @Test + void putBrowseConfigs_positive_classification() throws Exception { var typeId1 = UUID.randomUUID(); var typeId2 = UUID.randomUUID(); var config = new BrowseConfig().id(BrowseOptionType.LC) @@ -245,9 +258,9 @@ void putBrowseConfigs_positive() throws Exception { var stub = mockClassificationTypes(okapi.wireMockServer(), typeId1, typeId2); - doPut(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION, BrowseOptionType.LC), config); + doPut(ApiEndpoints.browseConfigPath(BrowseType.CLASSIFICATION, BrowseOptionType.LC), config); - var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION)) + var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.CLASSIFICATION)) .andExpect(jsonPath("$.totalRecords", is(3))); var configCollection = parseResponse(result, BrowseConfigCollection.class); @@ -257,6 +270,28 @@ void putBrowseConfigs_positive() throws Exception { okapi.wireMockServer().removeStub(stub); } + @Test + void putBrowseConfigs_positive_callNumber() throws Exception { + var typeId1 = UUID.randomUUID(); + var typeId2 = UUID.randomUUID(); + var config = new BrowseConfig().id(BrowseOptionType.SUDOC) + .shelvingAlgorithm(ShelvingOrderAlgorithmType.DEFAULT) + .addTypeIdsItem(typeId1).addTypeIdsItem(typeId2); + + var stub = mockCallNumberTypes(okapi.wireMockServer(), typeId1, typeId2); + + doPut(ApiEndpoints.browseConfigPath(BrowseType.CALL_NUMBER, BrowseOptionType.SUDOC), config); + + var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.CALL_NUMBER)) + .andExpect(jsonPath("$.totalRecords", is(6))); + + var configCollection = parseResponse(result, BrowseConfigCollection.class); + assertThat(configCollection.getConfigs()) + .hasSize(6) + .contains(config); + okapi.wireMockServer().removeStub(stub); + } + @Test void browseConfigs_synchronised_whenDeleteClassificationTypeEventReceived() { var typeId1 = UUID.randomUUID(); @@ -267,7 +302,7 @@ void browseConfigs_synchronised_whenDeleteClassificationTypeEventReceived() { final var stub = mockClassificationTypes(okapi.wireMockServer(), typeId1, typeId2); - doPut(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION, BrowseOptionType.LC), config); + doPut(ApiEndpoints.browseConfigPath(BrowseType.CLASSIFICATION, BrowseOptionType.LC), config); kafkaTemplate.send(inventoryClassificationTopic(), typeId1.toString(), new ResourceEvent() .type(ResourceEventType.DELETE) @@ -277,7 +312,7 @@ void browseConfigs_synchronised_whenDeleteClassificationTypeEventReceived() { ); await().atMost(ONE_MINUTE).pollInterval(TWO_SECONDS).untilAsserted(() -> { - var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION)); + var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.CLASSIFICATION)); var configCollection = parseResponse(result, BrowseConfigCollection.class); for (BrowseConfig browseConfig : configCollection.getConfigs()) { @@ -293,15 +328,51 @@ void browseConfigs_synchronised_whenDeleteClassificationTypeEventReceived() { } @Test - void referenceDataCacheInvalidates_whenClassificationTypeEventReceived() { + void browseConfigs_synchronised_whenDeleteCallNumberTypeEventReceived() { + var typeId1 = UUID.randomUUID(); + var typeId2 = UUID.randomUUID(); + var config = new BrowseConfig().id(BrowseOptionType.SUDOC) + .shelvingAlgorithm(ShelvingOrderAlgorithmType.DEFAULT) + .addTypeIdsItem(typeId2).addTypeIdsItem(typeId1); + + final var stub = mockCallNumberTypes(okapi.wireMockServer(), typeId1, typeId2); + + doPut(ApiEndpoints.browseConfigPath(BrowseType.CALL_NUMBER, BrowseOptionType.SUDOC), config); + + kafkaTemplate.send(inventoryCallNumberTopic(), typeId1.toString(), new ResourceEvent() + .type(ResourceEventType.DELETE) + .tenant(TENANT_ID) + .resourceName(ResourceType.CALL_NUMBER_TYPE.getName()) + .old(mapOf(ID_FIELD, typeId1.toString())) + ); + + await().atMost(ONE_MINUTE).pollInterval(TWO_SECONDS).untilAsserted(() -> { + var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.CALL_NUMBER)); + + var configCollection = parseResponse(result, BrowseConfigCollection.class); + for (var browseConfig : configCollection.getConfigs()) { + if (browseConfig.getId() == BrowseOptionType.SUDOC) { + assertThat(browseConfig.getTypeIds()) + .hasSize(1) + .containsExactly(typeId2); + } + } + }); + + okapi.wireMockServer().removeStub(stub); + } + + @ParameterizedTest + @CsvSource({ + INVENTORY_CLASSIFICATION_TYPE_TOPIC + ",classification-type", + INVENTORY_CALL_NUMBER_TYPE_TOPIC + ",call-number-type"}) + void referenceDataCacheInvalidates_whenEventReceived(String topic, String resource) { var cacheKey = "cache-test-key"; var referenceDataCache = Objects.requireNonNull(cacheManager.getCache(REFERENCE_DATA_CACHE)); referenceDataCache.put(cacheKey, UUID.randomUUID()); assertThat(referenceDataCache.get(cacheKey)).isNotNull(); - kafkaTemplate.send(inventoryClassificationTopic(), randomId(), new ResourceEvent() - .resourceName(ResourceType.CLASSIFICATION_TYPE.getName()) - ); + kafkaTemplate.send(getTopicName(topic), randomId(), new ResourceEvent().resourceName(resource)); await().atMost(ONE_MINUTE).pollInterval(TWO_SECONDS) .untilAsserted(() -> assertThat(referenceDataCache.get(cacheKey)).isNull()); diff --git a/src/test/java/org/folio/search/integration/KafkaMessageListenerTest.java b/src/test/java/org/folio/search/integration/KafkaMessageListenerTest.java index 4e1cb80e0..3d8a41092 100644 --- a/src/test/java/org/folio/search/integration/KafkaMessageListenerTest.java +++ b/src/test/java/org/folio/search/integration/KafkaMessageListenerTest.java @@ -17,6 +17,7 @@ import static org.folio.search.utils.TestConstants.TENANT_ID; import static org.folio.search.utils.TestConstants.inventoryAuthorityTopic; import static org.folio.search.utils.TestConstants.inventoryBoundWithTopic; +import static org.folio.search.utils.TestConstants.inventoryCallNumberTopic; import static org.folio.search.utils.TestConstants.inventoryClassificationTopic; import static org.folio.search.utils.TestConstants.inventoryHoldingTopic; import static org.folio.search.utils.TestConstants.inventoryInstanceTopic; @@ -39,6 +40,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.retry.support.RetryTemplate.defaultInstance; +import static org.testcontainers.shaded.org.apache.commons.lang3.StringUtils.EMPTY; import java.util.List; import java.util.concurrent.Callable; @@ -63,6 +65,7 @@ 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.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -315,24 +318,31 @@ void handleLinkedDataHubEvent_negative_logFailedEvent() { verify(batchProcessor).consumeBatchWithFallback(eq(expectedEvents), eq(KAFKA_RETRY_TEMPLATE_NAME), any(), any()); } - @Test - void handleClassificationTypeEvent_positive_filterOnlyDeleteEvents() { - var deleteEvent = resourceEvent(RESOURCE_ID, ResourceType.CLASSIFICATION_TYPE, DELETE, null, emptyMap()); - var createEvent = resourceEvent(RESOURCE_ID, ResourceType.CLASSIFICATION_TYPE, CREATE, emptyMap(), null); - var updateEvent = resourceEvent(RESOURCE_ID, ResourceType.CLASSIFICATION_TYPE, UPDATE, null, null); - - messageListener.handleClassificationTypeEvents(List.of( - classificationTypeConsumerRecord(deleteEvent), - classificationTypeConsumerRecord(updateEvent), - classificationTypeConsumerRecord(createEvent)) + @ParameterizedTest + @EnumSource(value = ResourceType.class, names = {"CLASSIFICATION_TYPE", "CALL_NUMBER_TYPE"}) + void handleBrowseConfigDataEvent_positive_filterOnlyDeleteEvents(ResourceType type) { + var deleteEvent = resourceEvent(RESOURCE_ID, type, DELETE, null, emptyMap()); + var createEvent = resourceEvent(RESOURCE_ID, type, CREATE, emptyMap(), null); + var updateEvent = resourceEvent(RESOURCE_ID, type, UPDATE, null, null); + + messageListener.handleBrowseConfigDataEvents(List.of( + consumerRecordForType(type, deleteEvent), + consumerRecordForType(type, updateEvent), + consumerRecordForType(type, createEvent)) ); - verify(configSynchronizationService).sync(List.of(deleteEvent), ResourceType.CLASSIFICATION_TYPE); + verify(configSynchronizationService).sync(List.of(deleteEvent), type); verify(batchProcessor).consumeBatchWithFallback(eq(List.of(deleteEvent)), any(), any(), any()); } @NotNull - private static ConsumerRecord classificationTypeConsumerRecord(ResourceEvent deleteEvent) { - return new ConsumerRecord<>(inventoryClassificationTopic(), 0, 0, RESOURCE_ID, deleteEvent); + private static ConsumerRecord consumerRecordForType(ResourceType resourceType, + ResourceEvent event) { + var topic = switch (resourceType) { + case CLASSIFICATION_TYPE -> inventoryClassificationTopic(); + case CALL_NUMBER_TYPE -> inventoryCallNumberTopic(); + default -> EMPTY; + }; + return new ConsumerRecord<>(topic, 0, 0, RESOURCE_ID, event); } } diff --git a/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java index 34e03aa22..1afd39c6f 100644 --- a/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java +++ b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java @@ -2,10 +2,11 @@ import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.search.client.InventoryReferenceDataClient.ReferenceDataType.CALL_NUMBER_TYPES; import static org.folio.search.client.InventoryReferenceDataClient.ReferenceDataType.CLASSIFICATION_TYPES; import static org.folio.search.domain.dto.BrowseOptionType.ALL; import static org.folio.search.domain.dto.BrowseOptionType.LC; -import static org.folio.search.domain.dto.BrowseType.INSTANCE_CLASSIFICATION; +import static org.folio.search.domain.dto.BrowseType.CLASSIFICATION; import static org.folio.search.model.client.CqlQueryParam.ID; import static org.folio.search.utils.TestUtils.randomId; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -22,6 +23,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import org.folio.search.client.InventoryReferenceDataClient.ReferenceDataType; import org.folio.search.converter.BrowseConfigMapper; import org.folio.search.domain.dto.BrowseConfig; import org.folio.search.domain.dto.BrowseConfigCollection; @@ -37,6 +39,8 @@ 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.EnumSource; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -57,14 +61,12 @@ class BrowseConfigServiceTest { @InjectMocks private BrowseConfigService service; - private BrowseType type; private BrowseOptionType configId; private BrowseConfig config; private List typeIds; @BeforeEach void setUp() { - type = INSTANCE_CLASSIFICATION; configId = LC; typeIds = List.of(randomId(), randomId()); config = new BrowseConfig() @@ -73,10 +75,11 @@ void setUp() { .typeIds(typeIds.stream().map(UUID::fromString).toList()); } - @Test - void shouldGetConfigs() { - List entities = List.of(getEntity()); - BrowseConfigCollection configs = new BrowseConfigCollection().addConfigsItem(config); + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldGetConfigs(BrowseType type) { + var entities = List.of(getEntity(type)); + var configs = new BrowseConfigCollection().addConfigsItem(config); given(repository.findByConfigId_BrowseType(type.getValue())).willReturn(entities); given(mapper.convert(entities)).willReturn(configs); @@ -86,10 +89,11 @@ void shouldGetConfigs() { verify(repository).findByConfigId_BrowseType(type.getValue()); } - @Test - void shouldGetConfig() { - var browseConfigId = new BrowseConfigId("instance-classification", "lc"); - var configEntity = getEntity(); + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldGetConfig(BrowseType type) { + var browseConfigId = new BrowseConfigId(type.getValue(), "lc"); + var configEntity = getEntity(type); given(repository.findById(browseConfigId)).willReturn(Optional.of(configEntity)); given(mapper.convert(configEntity)).willReturn(config); @@ -99,8 +103,9 @@ void shouldGetConfig() { verify(repository).findById(browseConfigId); } - @Test - void shouldThrowExceptionIfConfigNotExists() { + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldThrowExceptionIfConfigNotExists(BrowseType type) { given(repository.findById(any())).willReturn(Optional.empty()); var exception = assertThrows(IllegalStateException.class, () -> service.getConfig(type, configId)); @@ -110,11 +115,12 @@ void shouldThrowExceptionIfConfigNotExists() { assertTrue(exception.getMessage().contains(expectedMessage)); } - @Test - void shouldUpsertConfigWhenFitAllValidations() { - var entity = getEntity(); + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldUpsertConfigWhenFitAllValidations(BrowseType type) { + var entity = getEntity(type); given(mapper.convert(type, config)).willReturn(entity); - given(referenceDataService.fetchReferenceData(CLASSIFICATION_TYPES, ID, new HashSet<>(typeIds))) + given(referenceDataService.fetchReferenceData(referenceDataType(type), ID, new HashSet<>(typeIds))) .willReturn(new HashSet<>(typeIds)); assertDoesNotThrow(() -> service.upsertConfig(type, configId, config)); @@ -125,29 +131,32 @@ void shouldUpsertConfigWhenFitAllValidations() { void shouldThrowExceptionWhenConfigIdNotMatches() { config.setId(ALL); - var exception = assertThrows(RequestValidationException.class, () -> service.upsertConfig(type, configId, config)); + var exception = assertThrows(RequestValidationException.class, + () -> service.upsertConfig(CLASSIFICATION, configId, config)); assertThat(exception) .hasMessage("Body doesn't match path parameter: %s", configId.getValue()); verifyNoInteractions(repository); } - @Test - void shouldThrowExceptionWhenIdIsNotInReferenceData() { - given(referenceDataService.fetchReferenceData(CLASSIFICATION_TYPES, ID, new HashSet<>(typeIds))) + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldThrowExceptionWhenIdIsNotInReferenceData(BrowseType type) { + given(referenceDataService.fetchReferenceData(referenceDataType(type), ID, new HashSet<>(typeIds))) .willReturn(Set.of(typeIds.get(0))); var exception = assertThrows(RequestValidationException.class, () -> service.upsertConfig(type, configId, config)); assertThat(exception) - .hasMessage("Classification type IDs don't exist"); + .hasMessage(type.getValue() + " type IDs don't exist"); verifyNoInteractions(repository); } - @Test - void shouldDeleteTypeIdsFromConfigs() { - List entities = List.of(getEntity(), getEntity()); + @EnumSource(BrowseType.class) + @ParameterizedTest + void shouldDeleteTypeIdsFromConfigs(BrowseType type) { + var entities = List.of(getEntity(type), getEntity(type)); given(repository.findByConfigId_BrowseType(type.getValue())).willReturn(entities); ArgumentCaptor> captor = ArgumentCaptor.captor(); given(repository.saveAll(captor.capture())).willReturn(emptyList()); @@ -160,11 +169,15 @@ void shouldDeleteTypeIdsFromConfigs() { } } - private static BrowseConfigEntity getEntity() { + private static BrowseConfigEntity getEntity(BrowseType type) { var configEntity = new BrowseConfigEntity(); - configEntity.setConfigId(new BrowseConfigId(INSTANCE_CLASSIFICATION.getValue(), LC.getValue())); + configEntity.setConfigId(new BrowseConfigId(type.getValue(), LC.getValue())); configEntity.setShelvingAlgorithm(ShelvingOrderAlgorithmType.LC.getValue()); configEntity.setTypeIds(List.of("e1", "e2")); return configEntity; } + + private ReferenceDataType referenceDataType(BrowseType browseType) { + return CLASSIFICATION.equals(browseType) ? CLASSIFICATION_TYPES : CALL_NUMBER_TYPES; + } } diff --git a/src/test/java/org/folio/search/service/config/ConfigSynchronizationServiceTest.java b/src/test/java/org/folio/search/service/config/ConfigSynchronizationServiceTest.java index ce8328fbc..786325c5d 100644 --- a/src/test/java/org/folio/search/service/config/ConfigSynchronizationServiceTest.java +++ b/src/test/java/org/folio/search/service/config/ConfigSynchronizationServiceTest.java @@ -41,7 +41,15 @@ void shouldSyncClassificationTypeResources() { syncService.sync(resourceEvents, ResourceType.CLASSIFICATION_TYPE); var expectedIds = resourceEvents.stream().map(ResourceEvent::getId).toList(); - verify(configService).deleteTypeIdsFromConfigs(BrowseType.INSTANCE_CLASSIFICATION, expectedIds); + verify(configService).deleteTypeIdsFromConfigs(BrowseType.CLASSIFICATION, expectedIds); + } + + @Test + void shouldSyncCallNumberTypeResources() { + syncService.sync(resourceEvents, ResourceType.CALL_NUMBER_TYPE); + + var expectedIds = resourceEvents.stream().map(ResourceEvent::getId).toList(); + verify(configService).deleteTypeIdsFromConfigs(BrowseType.CALL_NUMBER, expectedIds); } @Test @@ -53,7 +61,7 @@ void shouldNotSync_WhenNullResourceType() { @NullAndEmptySource @ParameterizedTest - void shouldNotSync_WhenNullResourceType(List resourceEvents) { + void shouldNotSync_WhenNullResources(List resourceEvents) { syncService.sync(resourceEvents, ResourceType.CLASSIFICATION_TYPE); verify(configService, never()).deleteTypeIdsFromConfigs(any(), anyList()); diff --git a/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java index 840431e9f..ee750e2da 100644 --- a/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java +++ b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java @@ -1,19 +1,25 @@ package org.folio.search.service.consortium; import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.search.domain.dto.BrowseType.INSTANCE_CLASSIFICATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; import org.folio.search.domain.dto.BrowseConfig; import org.folio.search.domain.dto.BrowseConfigCollection; import org.folio.search.domain.dto.BrowseOptionType; +import org.folio.search.domain.dto.BrowseType; import org.folio.search.service.config.BrowseConfigService; import org.folio.spring.testing.type.UnitTest; -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.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -26,41 +32,52 @@ class BrowseConfigServiceDecoratorTest extends DecoratorBaseTest { private @Mock BrowseConfigService service; private @InjectMocks BrowseConfigServiceDecorator decorator; - @Test - void getConfigs() { + @ParameterizedTest + @EnumSource(BrowseType.class) + void getConfigs(BrowseType type) { var expected = new BrowseConfigCollection(); - when(service.getConfigs(INSTANCE_CLASSIFICATION)).thenReturn(expected); + when(service.getConfigs(type)).thenReturn(expected); mockExecutor(consortiumTenantExecutor); - var actual = decorator.getConfigs(INSTANCE_CLASSIFICATION); + var actual = decorator.getConfigs(type); assertThat(actual).isEqualTo(expected); - verify(service).getConfigs(INSTANCE_CLASSIFICATION); + verify(service).getConfigs(type); verify(consortiumTenantExecutor).execute(any()); } - @Test - void getConfig() { + @ParameterizedTest + @MethodSource("browseArguments") + void getConfig(BrowseType browseType, BrowseOptionType optionType) { var expected = new BrowseConfig(); - when(service.getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC)).thenReturn(expected); + when(service.getConfig(browseType, optionType)).thenReturn(expected); mockExecutor(consortiumTenantExecutor); - var actual = decorator.getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC); + var actual = decorator.getConfig(browseType, optionType); assertThat(actual).isEqualTo(expected); - verify(service).getConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC); + verify(service).getConfig(browseType, optionType); verify(consortiumTenantExecutor).execute(any()); } - @Test - void upsertConfig() { + @ParameterizedTest + @MethodSource("browseArguments") + void upsertConfig(BrowseType browseType, BrowseOptionType optionType) { var config = new BrowseConfig(); - doNothing().when(service).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + doNothing().when(service).upsertConfig(browseType, optionType, config); mockExecutorRun(consortiumTenantExecutor); - decorator.upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + decorator.upsertConfig(browseType, optionType, config); - verify(service).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + verify(service).upsertConfig(browseType, optionType, config); verify(consortiumTenantExecutor).run(any()); } + + private static Stream browseArguments() { + return Arrays.stream(BrowseType.values()) + .map(browseType -> Arrays.stream(BrowseOptionType.values()) + .map(optionType -> Arguments.of(browseType, optionType)) + .toList()) + .flatMap(Collection::stream); + } } diff --git a/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java b/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java index 4031482c6..049d25ee3 100644 --- a/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java +++ b/src/test/java/org/folio/search/utils/ShelvingOrderCalculationHelperTest.java @@ -13,30 +13,50 @@ class ShelvingOrderCalculationHelperTest { @Test void shouldCalculateLcNumber() { - String input = "HD1691 .I5 1967"; - String expectedShelfKey = "HD 41691 I5 41967"; + var input = "HD1691 .I5 1967"; + var expectedShelfKey = "HD 41691 I5 41967"; - String result = calculate(input, ShelvingOrderAlgorithmType.LC); + var result = calculate(input, ShelvingOrderAlgorithmType.LC); assertEquals(expectedShelfKey, result); } @Test void shouldCalculateDeweyNumber() { - String input = "302.55"; - String expectedShelfKey = "3302.55"; + var input = "302.55"; + var expectedShelfKey = "3302.55"; - String result = calculate(input, ShelvingOrderAlgorithmType.DEWEY); + var result = calculate(input, ShelvingOrderAlgorithmType.DEWEY); + + assertEquals(expectedShelfKey, result); + } + + @Test + void shouldCalculateNlmNumber() { + var input = "WB 102.5 B62 2018"; + var expectedShelfKey = "WB 3102.5 B62 42018"; + + var result = calculate(input, ShelvingOrderAlgorithmType.NLM); + + assertEquals(expectedShelfKey, result); + } + + @Test + void shouldCalculateSudocNumber() { + var input = "G1.16 A63 41581"; + var expectedShelfKey = "G 11 216 !A 263 !541581"; + + var result = calculate(input, ShelvingOrderAlgorithmType.SUDOC); assertEquals(expectedShelfKey, result); } @Test void shouldCalculateDefaultNumber() { - String input = "hd1691 ^I5 1967"; - String expectedShelfKey = "HD1691 ^I5 1967"; + var input = "hd1691 ^I5 1967"; + var expectedShelfKey = "HD1691 ^I5 1967"; - String result = calculate(input, ShelvingOrderAlgorithmType.DEFAULT); + var result = calculate(input, ShelvingOrderAlgorithmType.DEFAULT); assertEquals(expectedShelfKey, result); } diff --git a/src/test/java/org/folio/search/utils/TestConstants.java b/src/test/java/org/folio/search/utils/TestConstants.java index a4944ea9b..7d8951860 100644 --- a/src/test/java/org/folio/search/utils/TestConstants.java +++ b/src/test/java/org/folio/search/utils/TestConstants.java @@ -34,6 +34,7 @@ public class TestConstants { public static final String INVENTORY_HOLDING_TOPIC = "inventory.holdings-record"; public static final String INVENTORY_BOUND_WITH_TOPIC = "inventory.bound-with"; public static final String INVENTORY_CLASSIFICATION_TYPE_TOPIC = "inventory.classification-type"; + public static final String INVENTORY_CALL_NUMBER_TYPE_TOPIC = "inventory.call-number-type"; public static final String LINKED_DATA_WORK_INSTANCE = "linked-data.instance"; public static final String LINKED_DATA_WORK_TOPIC = "linked-data.work"; public static final String LINKED_DATA_HUB_TOPIC = "linked-data.hub"; @@ -112,6 +113,14 @@ public static String inventoryClassificationTopic(String tenantId) { return getTopicName(tenantId, INVENTORY_CLASSIFICATION_TYPE_TOPIC); } + public static String inventoryCallNumberTopic() { + return inventoryCallNumberTopic(TENANT_ID); + } + + public static String inventoryCallNumberTopic(String tenantId) { + return getTopicName(tenantId, INVENTORY_CALL_NUMBER_TYPE_TOPIC); + } + public static String inventoryContributorTopic() { return inventoryContributorTopic(TENANT_ID); } @@ -156,6 +165,10 @@ public static String indexName(String tenantId) { return String.join("_", ENV, ResourceType.INSTANCE.getName(), tenantId); } + public static String getTopicName(String topic) { + return String.format("%s.%s.%s", getFolioEnvName(), TENANT_ID, topic); + } + private static String getTopicName(String tenantId, String topic) { return String.format("%s.%s.%s", getFolioEnvName(), tenantId, topic); } diff --git a/src/test/java/org/folio/search/utils/TestUtils.java b/src/test/java/org/folio/search/utils/TestUtils.java index 9c04af7ba..79a3ca6d7 100644 --- a/src/test/java/org/folio/search/utils/TestUtils.java +++ b/src/test/java/org/folio/search/utils/TestUtils.java @@ -49,6 +49,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -678,6 +679,30 @@ public static MappingBuilder mockClassificationTypes(WireMockServer wireMockServ return stub; } + public static MappingBuilder mockCallNumberTypes(WireMockServer wireMockServer, UUID... typeIds) { + var strings = new LinkedList(); + var stub = get(urlPathEqualTo("/call-number-types")); + for (var typeId : typeIds) { + stub.withQueryParam("query", containing(typeId.toString())); + strings.add(""" + { + "id": "%s", + "name": "SUDOC" + } + """.formatted(typeId)); + } + stub.willReturn( + aResponse().withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE).withBody(""" + { + "callNumberTypes": [ + %s + ] + } + """.formatted(String.join(",", strings)))); + wireMockServer.stubFor(stub); + return stub; + } + private static JsonNode searchResponseWithAggregation(JsonNode aggregationValue) { return jsonObject("took", 0, "timed_out", false, "_shards", jsonObject("total", 1, "successful", 1, "skipped", 0, "failed", 0), "hits", diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 37446502d..f793294aa 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -97,6 +97,9 @@ folio: - name: inventory.classification-type numPartitions: 1 replicationFactor: 1 + - name: inventory.call-number-type + numPartitions: 1 + replicationFactor: 1 - name: inventory.location numPartitions: 1 replicationFactor: 1 @@ -142,10 +145,10 @@ folio: concurrency: 1 topic-pattern: (${folio.environment}\.)(.*\.)authorities\.authority group-id: ${folio.environment}-mod-search-authorities-group - classification-type: + browse-config-data: concurrency: 1 - topic-pattern: (${folio.environment}\.)(.*\.)inventory\.classification-type - group-id: ${folio.environment}-mod-search-classification-type-group + topic-pattern: (${folio.environment}\.)(.*\.)inventory\.(classification-type|call-number-type) + group-id: ${folio.environment}-mod-search-browse-config-data-group location: concurrency: 1 topic-pattern: (${folio.environment}\.)(.*\.)inventory\.(location|campus|institution|library)