diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml new file mode 100644 index 000000000..935cf0d5a --- /dev/null +++ b/.jpb/jpb-settings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 9b4697e40..42bc1876b 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -235,6 +235,36 @@ } ] }, + { + "id": "browse-config", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/browse/config/{browseType}", + "permissionsRequired": [ + "browse.config.collection.get" + ], + "modulePermissions": [ + "user-tenants.collection.get" + ] + }, + { + "methods": [ + "PUT" + ], + "pathPattern": "/browse/config/{browseType}/{browseConfigId}", + "permissionsRequired": [ + "browse.config.item.put" + ], + "modulePermissions": [ + "user-tenants.collection.get" + ] + } + ] + }, { "id": "search-config", "version": "0.2", @@ -531,6 +561,16 @@ "permissionName": "search.config.features.item.delete", "displayName": "Search - removes feature configuration", "description": "Removes feature configuration" + }, + { + "permissionName": "browse.config.collection.get", + "displayName": "Browse - returns configurations for browse type", + "description": "Returns configuration for browse type" + }, + { + "permissionName": "browse.config.item.put", + "displayName": "Browse - updates configuration entry for browse type", + "description": "Updates configuration entry for browse type" } ], "launchDescriptor": { diff --git a/src/main/java/org/folio/search/configuration/WebConfig.java b/src/main/java/org/folio/search/configuration/WebConfig.java index 01a1ca022..6fb656fb3 100644 --- a/src/main/java/org/folio/search/configuration/WebConfig.java +++ b/src/main/java/org/folio/search/configuration/WebConfig.java @@ -1,5 +1,7 @@ package org.folio.search.configuration; +import org.folio.search.domain.dto.BrowseOptionType; +import org.folio.search.domain.dto.BrowseType; import org.folio.search.domain.dto.CallNumberType; import org.folio.search.domain.dto.RecordType; import org.springframework.context.annotation.Configuration; @@ -14,6 +16,8 @@ public class WebConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToRecordTypeEnumConverter()); registry.addConverter(new StringToCallNumberTypeEnumConverter()); + registry.addConverter(new StringToBrowseTypeConverter()); + registry.addConverter(new StringToBrowseOptionTypeConverter()); } private static final class StringToRecordTypeEnumConverter implements Converter { @@ -29,4 +33,18 @@ public CallNumberType convert(String source) { return CallNumberType.valueOf(source.toUpperCase()); } } + + private static final class StringToBrowseTypeConverter implements Converter { + @Override + public BrowseType convert(String source) { + return BrowseType.fromValue(source.toLowerCase()); + } + } + + private static final class StringToBrowseOptionTypeConverter implements Converter { + @Override + public BrowseOptionType convert(String source) { + return BrowseOptionType.fromValue(source.toLowerCase()); + } + } } diff --git a/src/main/java/org/folio/search/controller/ConfigController.java b/src/main/java/org/folio/search/controller/ConfigController.java index ec742a2b3..9e32b5d39 100644 --- a/src/main/java/org/folio/search/controller/ConfigController.java +++ b/src/main/java/org/folio/search/controller/ConfigController.java @@ -6,12 +6,17 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +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.LanguageConfig; import org.folio.search.domain.dto.LanguageConfigs; import org.folio.search.domain.dto.TenantConfiguredFeature; import org.folio.search.rest.resource.ConfigApi; +import org.folio.search.service.consortium.BrowseConfigServiceDecorator; import org.folio.search.service.consortium.FeatureConfigServiceDecorator; import org.folio.search.service.consortium.LanguageConfigServiceDecorator; import org.springframework.http.ResponseEntity; @@ -28,6 +33,7 @@ public class ConfigController implements ConfigApi { private final LanguageConfigServiceDecorator languageConfigService; private final FeatureConfigServiceDecorator featureConfigService; + private final BrowseConfigServiceDecorator browseConfigService; @Override public ResponseEntity createLanguageConfig(@Valid LanguageConfig languageConfig) { @@ -36,8 +42,9 @@ public ResponseEntity createLanguageConfig(@Valid LanguageConfig } @Override - public ResponseEntity updateLanguageConfig(String code, LanguageConfig languageConfig) { - return ok(languageConfigService.update(code, languageConfig)); + public ResponseEntity deleteFeatureConfigurationById(String feature) { + featureConfigService.delete(TenantConfiguredFeature.fromValue(feature)); + return noContent().build(); } @Override @@ -47,14 +54,26 @@ public ResponseEntity deleteLanguageConfig(String code) { return noContent().build(); } + @Override + public ResponseEntity getAllFeatures() { + return ok(featureConfigService.getAll()); + } + @Override public ResponseEntity getAllLanguageConfigs() { return ok(languageConfigService.getAll()); } @Override - public ResponseEntity getAllFeatures() { - return ok(featureConfigService.getAll()); + public ResponseEntity getBrowseConfigs(BrowseType browseType) { + return ResponseEntity.ok(browseConfigService.getConfigs(browseType)); + } + + @Override + public ResponseEntity putBrowseConfig(BrowseType browseType, BrowseOptionType browseConfigId, + BrowseConfig browseConfig) { + browseConfigService.upsertConfig(browseType, browseConfigId, browseConfig); + return ResponseEntity.ok().build(); } @Override @@ -68,8 +87,8 @@ public ResponseEntity updateFeatureConfiguration(String feature, } @Override - public ResponseEntity deleteFeatureConfigurationById(String feature) { - featureConfigService.delete(TenantConfiguredFeature.fromValue(feature)); - return noContent().build(); + public ResponseEntity updateLanguageConfig(String code, LanguageConfig languageConfig) { + return ok(languageConfigService.update(code, languageConfig)); } + } diff --git a/src/main/java/org/folio/search/converter/BrowseConfigMapper.java b/src/main/java/org/folio/search/converter/BrowseConfigMapper.java new file mode 100644 index 000000000..22b710ae2 --- /dev/null +++ b/src/main/java/org/folio/search/converter/BrowseConfigMapper.java @@ -0,0 +1,35 @@ +package org.folio.search.converter; + +import java.util.List; +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.ShelvingOrderAlgorithmType; +import org.folio.search.model.config.BrowseConfigEntity; +import org.folio.search.model.config.BrowseConfigId; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring", + imports = {BrowseOptionType.class, BrowseConfigId.class, ShelvingOrderAlgorithmType.class}) +public interface BrowseConfigMapper { + + @Mapping(target = "configId", expression = "java(new BrowseConfigId(type.getValue(), config.getId().getValue()))") + @Mapping(target = "shelvingAlgorithm", expression = "java(config.getShelvingAlgorithm().getValue())") + BrowseConfigEntity convert(BrowseType type, BrowseConfig config); + + @Mapping(target = "id", expression = "java(BrowseOptionType.fromValue(source.getConfigId().getBrowseOptionType()))") + @Mapping(target = "shelvingAlgorithm", + expression = "java(ShelvingOrderAlgorithmType.fromValue(source.getShelvingAlgorithm()))") + BrowseConfig convert(BrowseConfigEntity source); + + default BrowseConfigCollection convert(List entities) { + return map(new BrowseConfigWrapper(entities)); + } + + @Mapping(target = "totalRecords", expression = "java(source.configs().size())") + BrowseConfigCollection map(BrowseConfigWrapper source); + + record BrowseConfigWrapper(List configs) { } +} diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java new file mode 100644 index 000000000..646ccbf65 --- /dev/null +++ b/src/main/java/org/folio/search/model/config/BrowseConfigEntity.java @@ -0,0 +1,60 @@ +package org.folio.search.model.config; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.proxy.HibernateProxy; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +@Table(name = "browse_config") +public class BrowseConfigEntity { + + @EmbeddedId + private BrowseConfigId configId; + + @Column(name = "shelving_algorithm", nullable = false) + private String shelvingAlgorithm; + + @Convert(converter = StringListConverter.class) + @Column(name = "type_ids", nullable = false) + private List typeIds = new ArrayList<>(); + + @Override + public final int hashCode() { + return Objects.hash(configId); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + Class effectiveClass = o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != effectiveClass) { + return false; + } + BrowseConfigEntity that = (BrowseConfigEntity) o; + return getConfigId() != null && Objects.equals(getConfigId(), that.getConfigId()); + } +} diff --git a/src/main/java/org/folio/search/model/config/BrowseConfigId.java b/src/main/java/org/folio/search/model/config/BrowseConfigId.java new file mode 100644 index 000000000..8336c8042 --- /dev/null +++ b/src/main/java/org/folio/search/model/config/BrowseConfigId.java @@ -0,0 +1,48 @@ +package org.folio.search.model.config; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.proxy.HibernateProxy; + +@Getter +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class BrowseConfigId { + + @Column(name = "browse_type") + private String browseType; + @Column(name = "browse_option_type") + private String browseOptionType; + + @Override + public final int hashCode() { + return Objects.hash(browseType, browseOptionType); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null) { + return false; + } + Class effectiveClass = o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != effectiveClass) { + return false; + } + BrowseConfigId that = (BrowseConfigId) o; + return browseType != null && Objects.equals(browseType, that.browseType) + && browseOptionType != null && Objects.equals(browseOptionType, that.browseOptionType); + } +} diff --git a/src/main/java/org/folio/search/model/config/StringListConverter.java b/src/main/java/org/folio/search/model/config/StringListConverter.java new file mode 100644 index 000000000..d7d2ce011 --- /dev/null +++ b/src/main/java/org/folio/search/model/config/StringListConverter.java @@ -0,0 +1,23 @@ +package org.folio.search.model.config; + +import static java.util.Collections.emptyList; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.Arrays; +import java.util.List; + +@Converter +public class StringListConverter implements AttributeConverter, String> { + private static final String SPLIT_CHAR = ";"; + + @Override + public String convertToDatabaseColumn(List stringList) { + return stringList != null ? String.join(SPLIT_CHAR, stringList) : ""; + } + + @Override + public List convertToEntityAttribute(String string) { + return string != null ? Arrays.asList(string.split(SPLIT_CHAR)) : emptyList(); + } +} diff --git a/src/main/java/org/folio/search/repository/BrowseConfigEntityRepository.java b/src/main/java/org/folio/search/repository/BrowseConfigEntityRepository.java new file mode 100644 index 000000000..fb38df14a --- /dev/null +++ b/src/main/java/org/folio/search/repository/BrowseConfigEntityRepository.java @@ -0,0 +1,11 @@ +package org.folio.search.repository; + +import java.util.List; +import org.folio.search.model.config.BrowseConfigEntity; +import org.folio.search.model.config.BrowseConfigId; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BrowseConfigEntityRepository extends JpaRepository { + + List findByConfigId_BrowseType(String browseType); +} diff --git a/src/main/java/org/folio/search/service/config/BrowseConfigService.java b/src/main/java/org/folio/search/service/config/BrowseConfigService.java new file mode 100644 index 000000000..7317bc6d5 --- /dev/null +++ b/src/main/java/org/folio/search/service/config/BrowseConfigService.java @@ -0,0 +1,35 @@ +package org.folio.search.service.config; + +import lombok.RequiredArgsConstructor; +import org.folio.search.converter.BrowseConfigMapper; +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.exception.RequestValidationException; +import org.folio.search.repository.BrowseConfigEntityRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BrowseConfigService { + + private static final String BODY_VALIDATION_MSG = "Body doesn't match path parameter: %s"; + + private final BrowseConfigEntityRepository repository; + private final BrowseConfigMapper mapper; + + public BrowseConfigCollection getConfigs(BrowseType type) { + return mapper.convert(repository.findByConfigId_BrowseType(type.getValue())); + } + + public void upsertConfig(BrowseType type, BrowseOptionType configId, BrowseConfig config) { + if (configId != null && configId != config.getId()) { + throw new RequestValidationException( + BODY_VALIDATION_MSG.formatted(configId.getValue()), "id", config.getId().toString()); + } + + var configEntity = mapper.convert(type, config); + repository.save(configEntity); + } +} diff --git a/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java new file mode 100644 index 000000000..59e3f657d --- /dev/null +++ b/src/main/java/org/folio/search/service/consortium/BrowseConfigServiceDecorator.java @@ -0,0 +1,26 @@ +package org.folio.search.service.consortium; + +import lombok.RequiredArgsConstructor; +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.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BrowseConfigServiceDecorator { + + private final ConsortiumTenantExecutor consortiumTenantExecutor; + private final BrowseConfigService browseConfigService; + + public BrowseConfigCollection getConfigs(BrowseType type) { + return consortiumTenantExecutor.execute(() -> browseConfigService.getConfigs(type)); + } + + public void upsertConfig(BrowseType type, BrowseOptionType configId, + BrowseConfig config) { + consortiumTenantExecutor.run(() -> browseConfigService.upsertConfig(type, configId, config)); + } +} diff --git a/src/main/resources/changelog/changelog-master.xml b/src/main/resources/changelog/changelog-master.xml index e8e346130..b0b22c9ba 100644 --- a/src/main/resources/changelog/changelog-master.xml +++ b/src/main/resources/changelog/changelog-master.xml @@ -8,4 +8,5 @@ + diff --git a/src/main/resources/changelog/changes/v3.2/create_browse_config_table.xml b/src/main/resources/changelog/changes/v3.2/create_browse_config_table.xml new file mode 100644 index 000000000..382f980ee --- /dev/null +++ b/src/main/resources/changelog/changes/v3.2/create_browse_config_table.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + Create browse_config table + + + + + + + + + + + + + + + + + + + + + + + Populate browse_config table with default values + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/swagger.api/mod-search.yaml b/src/main/resources/swagger.api/mod-search.yaml index fa45060ad..7d8156278 100644 --- a/src/main/resources/swagger.api/mod-search.yaml +++ b/src/main/resources/swagger.api/mod-search.yaml @@ -569,6 +569,44 @@ paths: '404': description: No feature configuration is found by id + /browse/config/{browseType}: + get: + operationId: getBrowseConfigs + description: Get all configurations for browse type + tags: + - config + parameters: + - $ref: '#/components/parameters/browse-type' + responses: + '200': + description: All browse configurations for type + content: + application/json: + schema: + $ref: '#/components/schemas/browseConfigCollection' + + /browse/config/{browseType}/{browseConfigId}: + put: + operationId: putBrowseConfig + description: Update configuration for browse type + tags: + - config + parameters: + - $ref: '#/components/parameters/browse-type' + - $ref: '#/components/parameters/browse-config-id' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/browseConfig' + responses: + '200': + description: Browse configuration has been added/updated. + '400': + $ref: '#/components/responses/badRequestResponse' + '422': + $ref: '#/components/responses/unprocessableEntityResponse' + components: schemas: instance: @@ -613,6 +651,46 @@ components: CallNumberType: enum: [ lc, dewey, nlm, sudoc, other, local ] type: string + browseType: + type: string + enum: + - instance-classification + browseOptionType: + type: string + enum: + - all + - lc + - dewey + shelvingOrderAlgorithmType: + type: string + enum: + - lc + - dewey + - default + browseConfig: + type: object + properties: + id: + description: Option ID + $ref: "#/components/schemas/browseOptionType" + shelvingAlgorithm: + description: Defines shelving order algorithm + $ref: "#/components/schemas/shelvingOrderAlgorithmType" + typeIds: + description: Type IDs that should be used by the option + type: array + items: + description: Classification type ID + type: string + browseConfigCollection: + type: object + properties: + configs: + type: array + items: + $ref: '#/components/schemas/browseConfig' + totalRecords: + type: integer responses: unprocessableEntityResponse: @@ -750,4 +828,17 @@ components: schema: type: boolean default: true - + browse-config-id: + name: browseConfigId + in: path + required: true + description: 'ID of browse config' + schema: + $ref: '#/components/schemas/browseOptionType' + browse-type: + name: browseType + in: path + required: true + description: 'Browse feature type' + schema: + $ref: '#/components/schemas/browseType' diff --git a/src/test/java/org/folio/search/controller/ConfigControllerTest.java b/src/test/java/org/folio/search/controller/ConfigControllerTest.java index 1d71e1fc2..d33b3cee2 100644 --- a/src/test/java/org/folio/search/controller/ConfigControllerTest.java +++ b/src/test/java/org/folio/search/controller/ConfigControllerTest.java @@ -1,10 +1,12 @@ package org.folio.search.controller; +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; import static org.folio.search.utils.TestUtils.languageConfig; import static org.folio.search.utils.TestUtils.mapOf; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.doNothing; @@ -19,22 +21,29 @@ import jakarta.persistence.EntityNotFoundException; import java.util.List; +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.FeatureConfig; import org.folio.search.domain.dto.FeatureConfigs; import org.folio.search.domain.dto.LanguageConfigs; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.folio.search.exception.RequestValidationException; import org.folio.search.exception.ValidationException; +import org.folio.search.service.consortium.BrowseConfigServiceDecorator; import org.folio.search.service.consortium.FeatureConfigServiceDecorator; import org.folio.search.service.consortium.LanguageConfigServiceDecorator; import org.folio.search.support.base.ApiEndpoints; import org.folio.spring.integration.XOkapiHeaders; import org.folio.spring.testing.type.UnitTest; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @UnitTest @Import(ApiExceptionHandler.class) @@ -47,6 +56,8 @@ class ConfigControllerTest { private LanguageConfigServiceDecorator languageConfigService; @MockBean private FeatureConfigServiceDecorator featureConfigService; + @MockBean + private BrowseConfigServiceDecorator browseConfigService; @Test void createLanguageConfig_positive() throws Exception { @@ -263,4 +274,37 @@ void deleteFeatureConfigurationById_positive() throws Exception { mockMvc.perform(request).andExpect(status().isNoContent()); } + + @Test + void getBrowseConfigs_positive() throws Exception { + var config = new BrowseConfig().id(BrowseOptionType.LC).shelvingAlgorithm(ShelvingOrderAlgorithmType.LC) + .typeIds(List.of("t1", "t2")); + when(browseConfigService.getConfigs(INSTANCE_CLASSIFICATION)) + .thenReturn(new BrowseConfigCollection().addConfigsItem(config).totalRecords(1)); + + var request = get(ApiEndpoints.browseConfigPath(INSTANCE_CLASSIFICATION)) + .header(XOkapiHeaders.TENANT, TENANT_ID) + .contentType(APPLICATION_JSON); + + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalRecords", is(1))) + .andExpect(jsonPath("$.configs[0].id", is(BrowseOptionType.LC.getValue()))) + .andExpect(jsonPath("$.configs[0].shelvingAlgorithm", is(ShelvingOrderAlgorithmType.LC.getValue()))) + .andExpect(jsonPath("$.configs[0].typeIds[*]", containsInAnyOrder(config.getTypeIds().toArray()))); + } + + @Test + void putBrowseConfig_positive() throws Exception { + var config = new BrowseConfig().id(BrowseOptionType.LC).shelvingAlgorithm(ShelvingOrderAlgorithmType.LC) + .typeIds(List.of("t1", "t2")); + doNothing().when(browseConfigService).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + + var request = put(ApiEndpoints.browseConfigPath(INSTANCE_CLASSIFICATION, BrowseOptionType.LC)) + .header(XOkapiHeaders.TENANT, TENANT_ID) + .contentType(APPLICATION_JSON); + + mockMvc.perform(request).andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.content().string(Matchers.emptyOrNullString())); + } } diff --git a/src/test/java/org/folio/search/controller/ConfigIT.java b/src/test/java/org/folio/search/controller/ConfigIT.java index 11dd1a89e..5f7253741 100644 --- a/src/test/java/org/folio/search/controller/ConfigIT.java +++ b/src/test/java/org/folio/search/controller/ConfigIT.java @@ -1,5 +1,6 @@ package org.folio.search.controller; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.folio.search.domain.dto.TenantConfiguredFeature.SEARCH_ALL_FIELDS; import static org.folio.search.sample.SampleInstances.getSemanticWebAsMap; @@ -11,9 +12,6 @@ import static org.folio.search.utils.TestUtils.parseResponse; import static org.folio.search.utils.TestUtils.randomId; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.opensearch.index.query.QueryBuilders.matchQuery; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -22,10 +20,15 @@ import java.util.List; import java.util.Map; import lombok.SneakyThrows; +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.Instance; import org.folio.search.domain.dto.LanguageConfig; import org.folio.search.domain.dto.LanguageConfigs; +import org.folio.search.domain.dto.ShelvingOrderAlgorithmType; import org.folio.search.support.base.ApiEndpoints; import org.folio.search.support.base.BaseIntegrationTest; import org.folio.search.utils.TestUtils; @@ -149,11 +152,11 @@ void shouldUseConfiguredLanguagesDuringMapping() { final var indexedInstance = getIndexedInstanceById(newInstance.getId()); - assertThat((Map) getMapValueByPath("title", indexedInstance), aMapWithSize(3)); - assertThat(getMapValueByPath("title.eng", indexedInstance), is(newInstance.getTitle())); - assertThat(getMapValueByPath("title.rus", indexedInstance), is(newInstance.getTitle())); - assertThat(getMapValueByPath("title.src", indexedInstance), is(newInstance.getTitle())); - assertThat(getMapValueByPath("title.fre", indexedInstance), nullValue()); + assertThat((Map) getMapValueByPath("title", indexedInstance)).hasSize(3); + assertThat(getMapValueByPath("title.eng", indexedInstance)).isEqualTo(newInstance.getTitle()); + assertThat(getMapValueByPath("title.rus", indexedInstance)).isEqualTo(newInstance.getTitle()); + assertThat(getMapValueByPath("title.src", indexedInstance)).isEqualTo(newInstance.getTitle()); + assertThat(getMapValueByPath("title.fre", indexedInstance)).isNull(); } @Test @@ -215,6 +218,32 @@ void deleteUnknownFeature_notExists() throws Exception { .andExpect(jsonPath("$.errors[0].message", is("Feature configuration not found for id: search.all.fields"))); } + @Test + void getBrowseConfigs_positive() throws Exception { + doGet(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalRecords", is(3))); + } + + @Test + void putBrowseConfigs_positive() throws Exception { + var config = new BrowseConfig().id(BrowseOptionType.LC) + .shelvingAlgorithm(ShelvingOrderAlgorithmType.DEFAULT) + .addTypeIdsItem("id1").addTypeIdsItem("id2"); + + doPut(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION, BrowseOptionType.LC), config) + .andExpect(status().isOk()); + + var result = doGet(ApiEndpoints.browseConfigPath(BrowseType.INSTANCE_CLASSIFICATION)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalRecords", is(3))); + + var configCollection = parseResponse(result, BrowseConfigCollection.class); + assertThat(configCollection.getConfigs()) + .hasSize(3) + .contains(config); + } + @SneakyThrows private Map getIndexedInstanceById(String id) { final var searchRequest = new SearchRequest() @@ -222,7 +251,7 @@ private Map getIndexedInstanceById(String id) { .indices(getIndexName(INSTANCE_RESOURCE, TENANT_ID)); await().until(() -> elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT) - .getHits().getTotalHits().value > 0); + .getHits().getTotalHits().value > 0); return elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT).getHits() .getAt(0).getSourceAsMap(); diff --git a/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java new file mode 100644 index 000000000..082f6a02d --- /dev/null +++ b/src/test/java/org/folio/search/service/config/BrowseConfigServiceTest.java @@ -0,0 +1,93 @@ +package org.folio.search.service.config; + +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.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import java.util.List; +import org.folio.search.converter.BrowseConfigMapper; +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.ShelvingOrderAlgorithmType; +import org.folio.search.exception.RequestValidationException; +import org.folio.search.model.config.BrowseConfigEntity; +import org.folio.search.model.config.BrowseConfigId; +import org.folio.search.repository.BrowseConfigEntityRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class BrowseConfigServiceTest { + + @Mock + private BrowseConfigEntityRepository repository; + + @Mock + private BrowseConfigMapper mapper; + + @InjectMocks + private BrowseConfigService service; + + private BrowseType type; + private BrowseOptionType configId; + private BrowseConfig config; + + @BeforeEach + void setUp() { + type = INSTANCE_CLASSIFICATION; + configId = LC; + config = new BrowseConfig().id(LC).shelvingAlgorithm(ShelvingOrderAlgorithmType.LC); + } + + @Test + void shouldGetConfigs() { + List entities = List.of(getEntity()); + BrowseConfigCollection configs = new BrowseConfigCollection().addConfigsItem(config); + given(repository.findByConfigId_BrowseType(type.getValue())).willReturn(entities); + given(mapper.convert(entities)).willReturn(configs); + + var result = service.getConfigs(type); + + assertEquals(configs, result); + verify(repository).findByConfigId_BrowseType(type.getValue()); + } + + @Test + void shouldUpsertConfigWhenConfigIdMatches() { + var entity = getEntity(); + given(mapper.convert(type, config)).willReturn(entity); + + assertDoesNotThrow(() -> service.upsertConfig(type, configId, config)); + verify(repository).save(entity); + } + + @Test + void shouldThrowExceptionWhenConfigIdNotMatches() { + config.setId(ALL); + + var exception = assertThrows(RequestValidationException.class, () -> service.upsertConfig(type, configId, config)); + + String expectedMessage = String.format("Body doesn't match path parameter: %s", configId.getValue()); + assertTrue(exception.getMessage().contains(expectedMessage)); + } + + private static BrowseConfigEntity getEntity() { + var configEntity = new BrowseConfigEntity(); + configEntity.setConfigId(new BrowseConfigId(INSTANCE_CLASSIFICATION.getValue(), LC.getValue())); + configEntity.setShelvingAlgorithm(ShelvingOrderAlgorithmType.LC.getValue()); + configEntity.setTypeIds(List.of("e1", "e2")); + return configEntity; + } +} diff --git a/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java new file mode 100644 index 000000000..339bce9a4 --- /dev/null +++ b/src/test/java/org/folio/search/service/consortium/BrowseConfigServiceDecoratorTest.java @@ -0,0 +1,53 @@ +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 org.folio.search.domain.dto.BrowseConfig; +import org.folio.search.domain.dto.BrowseConfigCollection; +import org.folio.search.domain.dto.BrowseOptionType; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class BrowseConfigServiceDecoratorTest extends DecoratorBaseTest { + + private @Mock ConsortiumTenantExecutor consortiumTenantExecutor; + private @Mock BrowseConfigService service; + private @InjectMocks BrowseConfigServiceDecorator decorator; + + @Test + void getConfigs() { + var expected = new BrowseConfigCollection(); + when(service.getConfigs(INSTANCE_CLASSIFICATION)).thenReturn(expected); + mockExecutor(consortiumTenantExecutor); + + var actual = decorator.getConfigs(INSTANCE_CLASSIFICATION); + + assertThat(actual).isEqualTo(expected); + verify(service).getConfigs(INSTANCE_CLASSIFICATION); + verify(consortiumTenantExecutor).execute(any()); + } + + @Test + void upsertConfig() { + var config = new BrowseConfig(); + doNothing().when(service).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + mockExecutorRun(consortiumTenantExecutor); + + decorator.upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + + verify(service).upsertConfig(INSTANCE_CLASSIFICATION, BrowseOptionType.LC, config); + verify(consortiumTenantExecutor).run(any()); + } +} 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 f17351b2f..fbf6b50d7 100644 --- a/src/test/java/org/folio/search/support/base/ApiEndpoints.java +++ b/src/test/java/org/folio/search/support/base/ApiEndpoints.java @@ -2,6 +2,8 @@ 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; @@ -53,6 +55,14 @@ public static String featureConfigPath(TenantConfiguredFeature feature) { return featureConfigPath() + "/" + feature.getValue(); } + public static String browseConfigPath(BrowseType type) { + return "/browse/config/" + type.getValue(); + } + + public static String browseConfigPath(BrowseType type, BrowseOptionType optionType) { + return "/browse/config/" + type.getValue() + "/" + optionType.getValue(); + } + public static String createIndicesPath() { return "/search/index/indices"; }