diff --git a/.gitignore b/.gitignore index 99fd8986..817f8489 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ replay_pid* application-local.yml target +index-data +index-data-test diff --git a/pom.xml b/pom.xml index 4f12bbfe..a95f6dbc 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ 1.9.23 1.6.3 2.15.4 + 9.10.0 @@ -113,6 +114,18 @@ 0.2.0 + + + org.apache.lucene + lucene-core + ${lucene.version} + + + org.apache.lucene + lucene-analysis-common + ${lucene.version} + + org.springframework.boot diff --git a/src/main/kotlin/no/nb/bikube/BikubeApplication.kt b/src/main/kotlin/no/nb/bikube/BikubeApplication.kt index d5ab281d..546cb56a 100644 --- a/src/main/kotlin/no/nb/bikube/BikubeApplication.kt +++ b/src/main/kotlin/no/nb/bikube/BikubeApplication.kt @@ -3,9 +3,11 @@ package no.nb.bikube import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling @ConfigurationPropertiesScan @SpringBootApplication +@EnableScheduling class BikubeApplication fun main(args: Array) { diff --git a/src/main/kotlin/no/nb/bikube/catalogue/collections/repository/CollectionsRepository.kt b/src/main/kotlin/no/nb/bikube/catalogue/collections/repository/CollectionsRepository.kt index 4a177f08..0888ab8a 100644 --- a/src/main/kotlin/no/nb/bikube/catalogue/collections/repository/CollectionsRepository.kt +++ b/src/main/kotlin/no/nb/bikube/catalogue/collections/repository/CollectionsRepository.kt @@ -33,12 +33,14 @@ class CollectionsRepository( ) } - fun getTitleByName(name: String): Mono { - return searchTexts( + fun getAllNewspaperTitles(page: Int = 1): Mono { + return getRecordsWebClientRequest( "record_type=${CollectionsRecordType.WORK} and " + - "work.description_type=${CollectionsDescriptionType.SERIAL} and " + - "title=\"${name}\"" - ) + "work.description_type=${CollectionsDescriptionType.SERIAL}", + CollectionsDatabase.TEXTS, + limit = 50, + from = (page-1) * 50 + 1 + ).bodyToMono() } fun searchPublisher(name: String): Mono { @@ -81,7 +83,12 @@ class CollectionsRepository( return getRecordsWebClientRequest(query, db).bodyToMono() } - private fun getRecordsWebClientRequest(query: String, db: CollectionsDatabase): WebClient.ResponseSpec { + private fun getRecordsWebClientRequest( + query: String, + db: CollectionsDatabase, + limit: Int = 10, + from: Int = 1 + ): WebClient.ResponseSpec { return webClient() .get() .uri { @@ -89,6 +96,8 @@ class CollectionsRepository( .queryParam("database", db.value) .queryParam("output", "json") .queryParam("search", query) + .queryParam("limit", limit) + .queryParam("startfrom", from) .build() } .retrieve() diff --git a/src/main/kotlin/no/nb/bikube/core/controller/CoreController.kt b/src/main/kotlin/no/nb/bikube/core/controller/CoreController.kt index 27d701bb..99bbd801 100644 --- a/src/main/kotlin/no/nb/bikube/core/controller/CoreController.kt +++ b/src/main/kotlin/no/nb/bikube/core/controller/CoreController.kt @@ -19,6 +19,7 @@ import no.nb.bikube.core.model.CatalogueRecord import no.nb.bikube.core.model.Item import no.nb.bikube.core.model.Title import no.nb.bikube.newspaper.service.NewspaperService +import no.nb.bikube.newspaper.service.TitleIndexService import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -33,7 +34,8 @@ import java.time.LocalDate @Tag(name = "Catalogue objects", description = "Endpoints related to catalog data for all text material") @RequestMapping("") class CoreController ( - private val newspaperService: NewspaperService + private val newspaperService: NewspaperService, + private val titleIndexService: TitleIndexService ){ companion object { const val DATE_REGEX = "^(17|18|19|20)\\d{2}(-)?(0[1-9]|1[0-2])(-)?(0[1-9]|[12][0-9]|3[01])$" @@ -86,10 +88,10 @@ class CoreController ( fun searchTitle( @RequestParam searchTerm: String, @RequestParam materialType: MaterialType - ): ResponseEntity> { + ): ResponseEntity> { if (searchTerm.isEmpty()) throw BadRequestBodyException("Search term cannot be empty.") return when(materialTypeToCatalogueName(materialType)) { - CatalogueName.COLLECTIONS -> ResponseEntity.ok(newspaperService.searchTitleByName(searchTerm)) + CatalogueName.COLLECTIONS -> ResponseEntity.ok(titleIndexService.searchTitle(searchTerm)) else -> throw NotSupportedException("Material type $materialType is not supported.") } } diff --git a/src/main/kotlin/no/nb/bikube/core/controller/GlobalControllerExceptionHandler.kt b/src/main/kotlin/no/nb/bikube/core/controller/GlobalControllerExceptionHandler.kt index a76e4933..c50da956 100644 --- a/src/main/kotlin/no/nb/bikube/core/controller/GlobalControllerExceptionHandler.kt +++ b/src/main/kotlin/no/nb/bikube/core/controller/GlobalControllerExceptionHandler.kt @@ -4,6 +4,7 @@ import jakarta.validation.ConstraintViolationException import no.nb.bikube.core.exception.BadRequestBodyException import no.nb.bikube.core.exception.NotSupportedException import no.nb.bikube.core.exception.RecordAlreadyExistsException +import no.nb.bikube.core.exception.SearchIndexNotAvailableException import no.nb.bikube.core.util.logger import org.springframework.http.HttpStatus import org.springframework.http.ProblemDetail @@ -58,6 +59,17 @@ class GlobalControllerExceptionHandler { return problemDetail } + + @ExceptionHandler + fun handlerSearchIndexNotAvailableException(exception: SearchIndexNotAvailableException): ProblemDetail { + logger().error("SearchIndexNotAvailableException occurred") + + val problemDetail = ProblemDetail.forStatus(HttpStatus.SERVICE_UNAVAILABLE) + problemDetail.detail = "The search index is unavailable" + problemDetail.addDefaultProperties() + + return problemDetail + } } fun ProblemDetail.addDefaultProperties() { diff --git a/src/main/kotlin/no/nb/bikube/core/exception/SearchIndexNotAvailableException.kt b/src/main/kotlin/no/nb/bikube/core/exception/SearchIndexNotAvailableException.kt new file mode 100644 index 00000000..fdaaa3e5 --- /dev/null +++ b/src/main/kotlin/no/nb/bikube/core/exception/SearchIndexNotAvailableException.kt @@ -0,0 +1,3 @@ +package no.nb.bikube.core.exception + +class SearchIndexNotAvailableException: Exception() diff --git a/src/main/kotlin/no/nb/bikube/newspaper/controller/TitleController.kt b/src/main/kotlin/no/nb/bikube/newspaper/controller/TitleController.kt index e53dd66e..6983a029 100644 --- a/src/main/kotlin/no/nb/bikube/newspaper/controller/TitleController.kt +++ b/src/main/kotlin/no/nb/bikube/newspaper/controller/TitleController.kt @@ -14,6 +14,7 @@ import no.nb.bikube.core.model.Title import no.nb.bikube.core.model.inputDto.TitleInputDto import no.nb.bikube.core.util.logger import no.nb.bikube.newspaper.service.NewspaperService +import no.nb.bikube.newspaper.service.TitleIndexService import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -27,7 +28,8 @@ import reactor.core.publisher.Mono @Tag(name="Newspaper titles", description="Endpoints related to newspaper titles.") @RequestMapping("/newspapers/titles") class TitleController ( - private val newspaperService: NewspaperService + private val newspaperService: NewspaperService, + private val titleIndexService: TitleIndexService ) { @PostMapping("/", produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation(summary = "Create a newspaper title") @@ -68,7 +70,10 @@ class TitleController ( return Mono.`when`(publisherMono, locationMono, languageMono) .then(newspaperService.createNewspaperTitle(title)) - .map { createdTitle -> ResponseEntity.status(HttpStatus.CREATED).body(createdTitle) } + .map { createdTitle -> + titleIndexService.addTitle(createdTitle) + ResponseEntity.status(HttpStatus.CREATED).body(createdTitle) + } .doOnSuccess { responseEntity -> logger().info("Newspaper title created with id: ${responseEntity.body?.catalogueId}") } diff --git a/src/main/kotlin/no/nb/bikube/newspaper/service/NewspaperService.kt b/src/main/kotlin/no/nb/bikube/newspaper/service/NewspaperService.kt index d03e9ce5..60db80a0 100644 --- a/src/main/kotlin/no/nb/bikube/newspaper/service/NewspaperService.kt +++ b/src/main/kotlin/no/nb/bikube/newspaper/service/NewspaperService.kt @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.publisher.SynchronousSink import reactor.kotlin.core.publisher.toMono +import reactor.util.function.Tuple2 import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -95,10 +96,20 @@ class NewspaperService ( } } - fun searchTitleByName(name: String): Flux { - return collectionsRepository.getTitleByName(name) - .flatMapIterable { it.getObjects() ?: emptyList() } - .map { mapCollectionsObjectToGenericTitle(it) } + fun getTitlesPage(pageNumber: Int): Mono, Int>> { + val pageContent = collectionsRepository.getAllNewspaperTitles(pageNumber) + .mapNotNull { model -> + model.getObjects() + ?. map { mapCollectionsObjectToGenericTitle(it) } + } + return Mono.zip(pageContent, Mono.just(pageNumber)) + } + + fun getAllTitles(): Mono> { + return getTitlesPage(1) + .expand { p -> getTitlesPage(p.t2 + 1) } + .flatMapIterable { it.t1 } + .collectList() } fun getItemsByTitle( diff --git a/src/main/kotlin/no/nb/bikube/newspaper/service/TitleIndexService.kt b/src/main/kotlin/no/nb/bikube/newspaper/service/TitleIndexService.kt new file mode 100644 index 00000000..270c83ae --- /dev/null +++ b/src/main/kotlin/no/nb/bikube/newspaper/service/TitleIndexService.kt @@ -0,0 +1,161 @@ +package no.nb.bikube.newspaper.service + +import no.nb.bikube.core.exception.SearchIndexNotAvailableException +import no.nb.bikube.core.model.Title +import no.nb.bikube.core.util.logger +import org.apache.lucene.analysis.core.LowerCaseFilterFactory +import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory +import org.apache.lucene.analysis.custom.CustomAnalyzer +import org.apache.lucene.document.Document +import org.apache.lucene.document.Field +import org.apache.lucene.document.StoredField +import org.apache.lucene.document.TextField +import org.apache.lucene.index.IndexWriter +import org.apache.lucene.index.IndexWriterConfig +import org.apache.lucene.index.Term +import org.apache.lucene.search.BooleanClause +import org.apache.lucene.search.BooleanQuery +import org.apache.lucene.search.SearcherManager +import org.apache.lucene.search.WildcardQuery +import org.apache.lucene.store.FSDirectory +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.nio.file.Paths +import java.time.LocalDate +import java.util.concurrent.atomic.AtomicInteger + +interface TitleIndexService { + fun indexAllTitles() + fun addTitle(title: Title) + fun searchTitle(query: String): List +} + +@ConditionalOnProperty( + prefix = "search-index", + name = ["enabled"], + havingValue = "true" +) +@Service +class TitleIndexServiceImpl( + private val newspaperService: NewspaperService, + @Value("\${search-index.path}") private val searchIndexPath: String +): TitleIndexService { + private val titleAnalyzer = CustomAnalyzer.builder() + .withTokenizer(WhitespaceTokenizerFactory.NAME) + .addTokenFilter(LowerCaseFilterFactory.NAME) + .build() + + private val indexWriter = IndexWriter( + FSDirectory.open(Paths.get(searchIndexPath)), + IndexWriterConfig(titleAnalyzer) + ) + + private val searcherManager = SearcherManager(indexWriter, null) + + private fun makeDocument(title: Title): Document? { + if (title.name == null) + return null + val document = Document() + document.add(TextField("name", title.name, Field.Store.YES)) + document.add(StoredField("catalogueId", title.catalogueId)) + title.startDate ?. let { document.add(StoredField("startDate", it.toString())) } + title.endDate ?. let { document.add(StoredField("endDate", it.toString())) } + title.publisher ?. let { document.add(StoredField("publisher", it)) } + title.publisherPlace ?. let { document.add(StoredField("publisherPlace", it)) } + title.language ?. let { document.add(StoredField("language", it)) } + title.materialType ?. let { document.add(StoredField("materialType", it)) } + return document + } + + private val indexStatus = AtomicInteger(IndexStatus.UNINITIALIZED.ordinal) + + @Scheduled( + initialDelayString = "\${search-index.initial-delay}", + fixedDelayString = "\${search-index.rebuild-index-delay}" + ) + override fun indexAllTitles() { + if (indexStatus.get() == IndexStatus.INDEXING.ordinal) + return + logger().debug("Start fetching all titles to index...") + newspaperService.getAllTitles() + .map { titles -> + titles.mapNotNull { makeDocument(it) } + } + .doOnSuccess { documents -> + indexStatus.set(IndexStatus.INDEXING.ordinal) + indexWriter.deleteAll() + indexWriter.addDocuments(documents) + indexWriter.commit() + searcherManager.maybeRefresh() + indexStatus.set(IndexStatus.READY.ordinal) + logger().info("Titles index ready") + } + .subscribe() + } + + override fun addTitle(title: Title) { + logger().debug("Adding title ${title.name} to index") + indexWriter.addDocument(makeDocument(title)) + indexWriter.commit() + searcherManager.maybeRefresh() + } + + @Throws(SearchIndexNotAvailableException::class) + override fun searchTitle(query: String): List<Title> { + if (indexStatus.get() != IndexStatus.READY.ordinal) + throw SearchIndexNotAvailableException() + val indexSearcher = searcherManager.acquire() + val terms = query.split(Regex("\\s+")) + val queryBuilder = BooleanQuery.Builder() + terms.forEach { + queryBuilder.add( + WildcardQuery(Term("name", "*${it.lowercase()}*")), + BooleanClause.Occur.MUST + ) + } + + val q = queryBuilder.build() + logger().debug("Title search: {}", q) + val storedFields = indexSearcher.storedFields() + return indexSearcher.search(q, 50) + .scoreDocs + .map { storedFields.document(it.doc) } + .map { doc -> + Title( + catalogueId = doc.get("catalogueId"), + name = doc.get("name"), + startDate = doc.get("startDate") ?. let { LocalDate.parse(it) }, + endDate = doc.get("endDate") ?. let { LocalDate.parse(it) }, + publisher = doc.get("publisher"), + publisherPlace = doc.get("publisherPlace"), + language = doc.get("language"), + materialType = doc.get("materialType") + ) + } + } + + @Scheduled(fixedDelayString = "\${search-index.searcher-refresh-delay}") + fun refresh() { + searcherManager.maybeRefresh() + } +} + +@ConditionalOnProperty( + prefix = "search-index", + name = ["enabled"], + havingValue = "false" +) +@Service +class TitleIndexServiceDisabledImpl: TitleIndexService { + override fun indexAllTitles() {} + override fun addTitle(title: Title) {} + override fun searchTitle(query: String) = emptyList<Title>() +} + +enum class IndexStatus { + UNINITIALIZED, + INDEXING, + READY +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a3f7bb19..af1175e2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -43,3 +43,10 @@ alma: http-proxy: host: "" port: 0 + +search-index: + enabled: true + path: index-data + initial-delay: 1000 + rebuild-index-delay: 1800000 + searcher-refresh-delay: 600000 diff --git a/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerIntegrationTest.kt b/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerIntegrationTest.kt index 46798a83..2785d371 100644 --- a/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerIntegrationTest.kt +++ b/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerIntegrationTest.kt @@ -18,6 +18,7 @@ import no.nb.bikube.core.enum.MaterialType import no.nb.bikube.core.model.Item import no.nb.bikube.core.model.Title import no.nb.bikube.core.util.DateUtils +import no.nb.bikube.newspaper.service.TitleIndexService import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -41,6 +42,9 @@ class CoreControllerIntegrationTest ( @MockkBean private lateinit var collectionsRepository: CollectionsRepository + @MockkBean + private lateinit var titleIndexService: TitleIndexService + private val titleId = "1" private val yearWorkId = "2" private val manifestationId = "3" @@ -56,8 +60,9 @@ class CoreControllerIntegrationTest ( every { collectionsRepository.getSingleCollectionsModel(yearWorkId) } returns Mono.just(collectionsModelMockYearWorkA.copy()) every { collectionsRepository.getSingleCollectionsModel(manifestationId) } returns Mono.just(collectionsModelMockManifestationA.copy()) every { collectionsRepository.getSingleCollectionsModel(itemId) } returns Mono.just(collectionsModelMockItemA.copy()) - every { collectionsRepository.getTitleByName(any()) } returns Mono.just(collectionsModelMockAllTitles.copy()) every { collectionsRepository.getWorkYearForTitle(any(), any()) } returns Mono.just(collectionsModelMockYearWorkA.copy()) + every { titleIndexService.searchTitle(any()) } returns + collectionsModelMockAllTitles.getObjects()!!.map { mapCollectionsObjectToGenericTitle(it) } } private fun getItem(itemId: String, materialType: MaterialType): ResponseSpec { @@ -331,7 +336,7 @@ class CoreControllerIntegrationTest ( @Test fun `get-title-search endpoint should return empty flux when no items match search term`() { - every { collectionsRepository.getTitleByName("no match") } returns Mono.just(collectionsModelEmptyRecordListMock.copy()) + every { titleIndexService.searchTitle("no match") } returns emptyList() searchTitle("no match", MaterialType.NEWSPAPER) .returnResult<Title>() diff --git a/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerTest.kt b/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerTest.kt index 68c7f0a9..b239395f 100644 --- a/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerTest.kt +++ b/src/test/kotlin/no/nb/bikube/core/controller/CoreControllerTest.kt @@ -7,10 +7,12 @@ import io.mockk.verify import no.nb.bikube.core.enum.MaterialType import no.nb.bikube.core.exception.BadRequestBodyException import no.nb.bikube.core.exception.NotSupportedException +import no.nb.bikube.core.model.Title import no.nb.bikube.newspaper.NewspaperMockData.Companion.newspaperItemMockA import no.nb.bikube.newspaper.NewspaperMockData.Companion.newspaperTitleMockA import no.nb.bikube.newspaper.NewspaperMockData.Companion.newspaperTitleMockB import no.nb.bikube.newspaper.service.NewspaperService +import no.nb.bikube.newspaper.service.TitleIndexService import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -30,6 +32,9 @@ class CoreControllerTest { @MockkBean private lateinit var newspaperService: NewspaperService + @MockkBean + private lateinit var titleIndexService: TitleIndexService + @Test fun `get single item for newspaper should return item in body`() { every { newspaperService.getSingleItem(any()) } returns Mono.just(newspaperItemMockA.copy()) @@ -88,20 +93,16 @@ class CoreControllerTest { @Test fun `search title should return a list of titles matching name`() { - every { newspaperService.searchTitleByName(any()) } returns Flux.just( + every { titleIndexService.searchTitle(any()) } returns listOf( newspaperTitleMockA.copy(), newspaperTitleMockB.copy() ) - coreController.searchTitle("Avis", MaterialType.NEWSPAPER).body!! - .test() - .expectSubscription() - .assertNext { - Assertions.assertEquals(newspaperTitleMockA, it) - } - .assertNext { - Assertions.assertEquals(newspaperTitleMockB, it) - } - .verifyComplete() + Assertions.assertEquals( + coreController.searchTitle("Avis", MaterialType.NEWSPAPER).body!!, + listOf( + newspaperTitleMockA.copy(), newspaperTitleMockB.copy() + ) + ) } @Test @@ -110,18 +111,19 @@ class CoreControllerTest { assertThrows<NotSupportedException> { coreController.searchTitle("Avis", MaterialType.PERIODICAL) } assertThrows<NotSupportedException> { coreController.searchTitle("Avis", MaterialType.MONOGRAPH) } - verify { newspaperService.searchTitleByName(any()) wasNot Called } + verify(exactly = 0) { titleIndexService.searchTitle(any()) } } @Test - fun `search title should call on newspaperService function when materialType is NEWSPAPER`() { - every { newspaperService.searchTitleByName(any()) } returns Flux.empty() + fun `search title should call on titleIndexService function when materialType is NEWSPAPER`() { + every { titleIndexService.searchTitle(any()) } returns emptyList() - coreController.searchTitle("Avis", MaterialType.NEWSPAPER).body!! - .test() - .verifyComplete() + Assertions.assertEquals( + coreController.searchTitle("Avis", MaterialType.NEWSPAPER).body!!, + emptyList<Title>() + ) - verify { newspaperService.searchTitleByName(any()) } + verify(exactly = 1) { titleIndexService.searchTitle(any()) } } @Test diff --git a/src/test/kotlin/no/nb/bikube/newspaper/service/NewspaperServiceTest.kt b/src/test/kotlin/no/nb/bikube/newspaper/service/NewspaperServiceTest.kt index dcf3f914..8ff502b2 100644 --- a/src/test/kotlin/no/nb/bikube/newspaper/service/NewspaperServiceTest.kt +++ b/src/test/kotlin/no/nb/bikube/newspaper/service/NewspaperServiceTest.kt @@ -441,42 +441,6 @@ class NewspaperServiceTest( .verify() } - @Test - fun `getTitleByName should return correctly mapped title`() { - every { collectionsRepository.getTitleByName(any()) } returns Mono.just(collectionsModelMockTitleA) - - newspaperService.searchTitleByName("1") - .test() - .expectSubscription() - .assertNext { - Assertions.assertEquals( - Title( - name = collectionsModelMockTitleA.getFirstObject()!!.getName(), - startDate = collectionsModelMockTitleA.getFirstObject()!!.getStartDate(), - endDate = null, - publisher = collectionsModelMockTitleA.getFirstObject()!!.getPublisher(), - publisherPlace = collectionsModelMockTitleA.getFirstObject()!!.getPublisherPlace(), - language = collectionsModelMockTitleA.getFirstObject()!!.getLanguage(), - materialType = collectionsModelMockTitleA.getFirstObject()!!.getMaterialType()!!.norwegian, - catalogueId = collectionsModelMockTitleA.getFirstObject()!!.priRef - ), - it - ) - } - .verifyComplete() - } - - @Test - fun `getTitleByName should return empty Mono if no titles are found`() { - every { collectionsRepository.getTitleByName(any()) } returns Mono.just(collectionsModelEmptyRecordListMock) - - newspaperService.searchTitleByName("1") - .test() - .expectSubscription() - .expectNextCount(0) - .verifyComplete() - } - @Test fun `createTitle should return correctly mapped record`() { every { collectionsRepository.createTextsRecord(any()) } returns Mono.just(collectionsModelMockTitleE) @@ -503,26 +467,6 @@ class NewspaperServiceTest( verify { collectionsRepository.createTextsRecord(titleEncodedDto) } } - @Test - fun `searchTitleByName should return a correctly mapped catalogue record`() { - every { collectionsRepository.getTitleByName(any()) } returns Mono.just(collectionsModelMockTitleE) - newspaperService.searchTitleByName("1") - .test() - .expectSubscription() - .assertNext { Assertions.assertEquals(newspaperTitleMockB, it) } - .verifyComplete() - } - - @Test - fun `searchTitleByName should return a flux of an empty list if no titles are found`() { - every { collectionsRepository.getTitleByName(any()) } returns Mono.empty() - newspaperService.searchTitleByName("1") - .test() - .expectSubscription() - .expectNextCount(0) - .verifyComplete() - } - @Test fun `createPublisher should return RecordAlreadyExistsException if searchPublisher returns non-empty list`() { every { collectionsRepository.searchPublisher(any()) } returns Mono.just(collectionsNameModelMockA) diff --git a/src/test/kotlin/no/nb/bikube/newspaper/service/TitleIndexServiceTest.kt b/src/test/kotlin/no/nb/bikube/newspaper/service/TitleIndexServiceTest.kt new file mode 100644 index 00000000..f325d35f --- /dev/null +++ b/src/test/kotlin/no/nb/bikube/newspaper/service/TitleIndexServiceTest.kt @@ -0,0 +1,76 @@ +package no.nb.bikube.newspaper.service + +import com.ninjasquad.springmockk.MockkBean +import io.mockk.every +import no.nb.bikube.catalogue.collections.CollectionsModelMockData.Companion.collectionsModelMockAllTitles +import no.nb.bikube.catalogue.collections.mapper.mapCollectionsObjectToGenericTitle +import no.nb.bikube.catalogue.collections.model.getObjects +import no.nb.bikube.newspaper.NewspaperMockData.Companion.newspaperTitleInputDtoMockA +import no.nb.bikube.newspaper.NewspaperMockData.Companion.newspaperTitleMockA +import no.nb.bikube.newspaper.controller.TitleController +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.TestPropertySource +import reactor.core.publisher.Mono + +@SpringBootTest +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestPropertySource( + properties = [ + "search-index.enabled=true", + "search-index.path=index-data-test", + ] +) +class TitleIndexServiceTest( + @Autowired private val titleIndexService: TitleIndexService, + @Autowired private val titleController: TitleController +) { + @MockkBean + private lateinit var newspaperService: NewspaperService + + @BeforeAll + fun mockTitleList() { + every { newspaperService.getAllTitles() } returns + Mono.just( + collectionsModelMockAllTitles + .getObjects()!! + .map { mapCollectionsObjectToGenericTitle(it) } + ) + titleIndexService.indexAllTitles() + } + + @Test + fun `All titles should be indexed and searchable`() { + Assertions.assertEquals( + titleIndexService.searchTitle("avis").size, + 5 + ) + } + + @Test + fun `A newly created title should be searchable immediately`() { + Assertions.assertEquals( + titleIndexService.searchTitle("Unique title").size, + 0 + ) + every { newspaperService.createPublisher(any(), any()) } returns Mono.empty() + every { newspaperService.createPublisherPlace(any(), any()) } returns Mono.empty() + every { newspaperService.createLanguage(any(), any()) } returns Mono.empty() + every { newspaperService.createNewspaperTitle(any()) } returns + Mono.just(newspaperTitleMockA.copy(name = "Unique title")) + + titleController.createTitle(newspaperTitleInputDtoMockA) + .subscribe() + + Assertions.assertEquals( + titleIndexService.searchTitle("Unique title").size, + 1 + ) + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index eb490f24..5364f1e8 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,2 +1,7 @@ alma: api-key: SECRETKEY + +search-index: + enabled: false + +app.scheduling.enabled: false