From f51aa0c090a9c6a75ea88cd8019b8324e30dcc44 Mon Sep 17 00:00:00 2001 From: Viacheslav Poliakov <159778173+viacheslavpoliakov@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:44:29 +0200 Subject: [PATCH] feat(authority-source-files): produce authority source file update event (#271) * feat(authority-source-files): produce domain event - Add Kafka topic for authority source files updating - Add publisher for new topic Closes: MODELINKS-202 --- NEWS.md | 1 + descriptors/ModuleDescriptor-template.json | 10 +++ doc/documentation.md | 2 + .../entlinks/config/KafkaConfiguration.java | 18 ++-- .../AuthoritySourceFileServiceDelegate.java | 43 ++++++--- .../AuthorityDomainEventPublisher.java | 2 + ...thoritySourceFileDomainEventPublisher.java | 37 ++++++++ .../authority/AuthoritySourceFileService.java | 48 +++++++--- ...AuthoritySourceFilePropagationService.java | 11 ++- .../ConsortiumPropagationService.java | 1 - .../AuthoritySourceFilePropagationData.java | 8 ++ src/main/resources/application.yaml | 3 + ...uthoritySourceFileServiceDelegateTest.java | 88 +++++++++++++++++-- ...itySourceFileDomainEventPublisherTest.java | 67 ++++++++++++++ .../AuthoritySourceFileServiceTest.java | 12 +-- ...oritySourceFilePropagationServiceTest.java | 18 ++-- src/test/resources/application-test.yaml | 3 + 17 files changed, 321 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisher.java create mode 100644 src/main/java/org/folio/entlinks/service/consortium/propagation/model/AuthoritySourceFilePropagationData.java create mode 100644 src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisherTest.java diff --git a/NEWS.md b/NEWS.md index 3dec1b27..ae60abec 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,6 +23,7 @@ * Implement endpoint for bulk authorities upsert from external file ([MODELINKS-173](https://issues.folio.org/browse/MODELINKS-173)) * Add possibility to filter Authority records by (un)defined fields in Cql query ([MODELINKS-214](https://issues.folio.org/browse/MODELINKS-214)) * Set auto_linking_enabled in instance_authority_linking_rule for 6xx fields ([MODELINKS-220](https://folio-org.atlassian.net/browse/MODELINKS-220)) +* Add Authority source file Kafka topic and publisher for update event ([MODELINKS-202](https://folio-org.atlassian.net/browse/MODELINKS-202)) ### Bug fixes * Fix secure setup of system users by default ([MODELINKS-135](https://issues.folio.org/browse/MODELINKS-135)) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index eb756329..ca449395 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -807,6 +807,16 @@ "value": "50", "description": "Maximum number of records returned in a single call to poll()." }, + { + "name": "KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_PARTITIONS", + "value": "1", + "description": "Amount of partitions for `authority-authority-source-file` topic." + }, + { + "name": "KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_REPLICATION_FACTOR", + "value": "", + "description": "Replication factor for `authority-authority-source-file` topic." + }, { "name": "KAFKA_INSTANCE_AUTHORITY_TOPIC_PARTITIONS", "value": "10", diff --git a/doc/documentation.md b/doc/documentation.md index 445f0180..ef6a98fd 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -92,6 +92,8 @@ docker run -t -i -p 8081:8081 mod-entities-links | KAFKA_SSL_TRUSTSTORE_LOCATION | - | The location of the Kafka trust store file. | | KAFKA_SSL_TRUSTSTORE_PASSWORD | - | The password for the Kafka trust store file. If a password is not set, trust store file configured will still be used, but integrity checking is disabled. | | KAFKA_CONSUMER_MAX_POLL_RECORDS | 50 | Maximum number of records returned in a single call to poll(). | +| KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_PARTITIONS | 1 | Amount of partitions for `authority.authority-source-file` topic. | +| KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_REPLICATION_FACTOR | - | Replication factor for `authority.authority-source-file` topic. | | KAFKA_INSTANCE_AUTHORITY_TOPIC_PARTITIONS | 10 | Amount of partitions for `links.instance-authority` topic. | | KAFKA_INSTANCE_AUTHORITY_TOPIC_REPLICATION_FACTOR | - | Replication factor for `links.instance-authority` topic. | | KAFKA_INSTANCE_AUTHORITY_STATS_TOPIC_PARTITIONS | 10 | Amount of partitions for `links.instance-authority-stats` topic. | diff --git a/src/main/java/org/folio/entlinks/config/KafkaConfiguration.java b/src/main/java/org/folio/entlinks/config/KafkaConfiguration.java index 2561773a..dcd05aca 100644 --- a/src/main/java/org/folio/entlinks/config/KafkaConfiguration.java +++ b/src/main/java/org/folio/entlinks/config/KafkaConfiguration.java @@ -150,22 +150,28 @@ public EventProducer linkUpdateReportMessageProducerService( } @Bean - public ProducerFactory> domainProducerFactory(KafkaProperties kafkaProperties) { + public ProducerFactory> domainProducerFactory(KafkaProperties kafkaProperties) { return getProducerConfigProps(kafkaProperties); } @Bean - public KafkaTemplate> domainKafkaTemplate( - ProducerFactory> domainProducerFactory) { + public KafkaTemplate> domainKafkaTemplate( + ProducerFactory> domainProducerFactory) { return new KafkaTemplate<>(domainProducerFactory); } - @Bean - public EventProducer> authorityDomainMessageProducerService( - KafkaTemplate> template) { + @Bean("authorityDomainMessageProducer") + public EventProducer> authorityDomainMessageProducerService( + KafkaTemplate> template) { return new EventProducer<>(template, "authorities.authority"); } + @Bean("authoritySourceFileDomainMessageProducer") + public EventProducer> authoritySourceFileDomainMessageProducerService( + KafkaTemplate> template) { + return new EventProducer<>(template, "authority.authority-source-file"); + } + private ConcurrentKafkaListenerContainerFactory listenerFactory( ConsumerFactory consumerFactory) { var factory = new ConcurrentKafkaListenerContainerFactory(); diff --git a/src/main/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegate.java b/src/main/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegate.java index 6b92d0a4..c8d43256 100644 --- a/src/main/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegate.java +++ b/src/main/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegate.java @@ -9,6 +9,7 @@ import java.util.LinkedList; import java.util.List; import java.util.UUID; +import java.util.function.BiConsumer; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; @@ -21,13 +22,16 @@ import org.folio.entlinks.domain.entity.AuthoritySourceFile; import org.folio.entlinks.exception.RequestBodyValidationException; import org.folio.entlinks.integration.dto.event.DomainEventType; +import org.folio.entlinks.service.authority.AuthoritySourceFileDomainEventPublisher; import org.folio.entlinks.service.authority.AuthoritySourceFileService; import org.folio.entlinks.service.consortium.ConsortiumTenantsService; import org.folio.entlinks.service.consortium.UserTenantsService; -import org.folio.entlinks.service.consortium.propagation.ConsortiumPropagationService; +import org.folio.entlinks.service.consortium.propagation.ConsortiumAuthoritySourceFilePropagationService; +import org.folio.entlinks.service.consortium.propagation.model.AuthoritySourceFilePropagationData; import org.folio.spring.FolioExecutionContext; import org.folio.spring.service.SystemUserScopedExecutionService; import org.folio.tenant.domain.dto.Parameter; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; @Log4j2 @@ -40,7 +44,8 @@ public class AuthoritySourceFileServiceDelegate { private final AuthoritySourceFileService service; private final AuthoritySourceFileMapper mapper; private final UserTenantsService tenantsService; - private final ConsortiumPropagationService propagationService; + private final AuthoritySourceFileDomainEventPublisher eventPublisher; + private final ConsortiumAuthoritySourceFilePropagationService propagationService; private final FolioExecutionContext context; private final SystemUserScopedExecutionService executionService; private final ConsortiumTenantsService consortiumTenantsService; @@ -64,7 +69,7 @@ public AuthoritySourceFileDto createAuthoritySourceFile(AuthoritySourceFilePostD service.createSequence(created.getSequenceName(), created.getHridStartNumber()); - propagationService.propagate(entity, CREATE, context.getTenantId()); + propagationService.propagate(getPropagationData(entity, null), CREATE, context.getTenantId()); return mapper.toDto(created); } @@ -72,14 +77,18 @@ public void patchAuthoritySourceFile(UUID id, AuthoritySourceFilePatchDto partia log.debug("patch:: Attempting to patch AuthoritySourceFile [id: {}, patchDto: {}]", id, partiallyModifiedDto); var existingEntity = service.getById(id); validateActionRightsForTenant(DomainEventType.UPDATE); - validatePatchRequest(partiallyModifiedDto, existingEntity); + var hasAuthorityReferences = anyAuthoritiesExistForSourceFile(existingEntity); + validatePatchRequest(partiallyModifiedDto, existingEntity, hasAuthorityReferences); var partialEntityUpdate = new AuthoritySourceFile(existingEntity); partialEntityUpdate = mapper.partialUpdate(partiallyModifiedDto, partialEntityUpdate); normalizeBaseUrl(partialEntityUpdate); - var patched = service.update(id, partialEntityUpdate); + + var publishConsumer = publishRequired(hasAuthorityReferences, partiallyModifiedDto, existingEntity) + ? getUpdatePublishConsumer() : null; + var patched = service.update(id, partialEntityUpdate, publishConsumer); log.debug("patch:: Authority Source File partially updated: {}", patched); - propagationService.propagate(patched, UPDATE, context.getTenantId()); + propagationService.propagate(getPropagationData(patched, publishConsumer), UPDATE, context.getTenantId()); } public void deleteAuthoritySourceFileById(UUID id) { @@ -88,14 +97,14 @@ public void deleteAuthoritySourceFileById(UUID id) { if (anyAuthoritiesExistForSourceFile(entity)) { throw new RequestBodyValidationException( - "Unable to delete. Authority source file has referenced authorities", Collections.emptyList()); + "Unable to delete. Authority source file has referenced authorities", Collections.emptyList()); } if (entity.getSequenceName() != null) { service.deleteSequence(entity.getSequenceName()); } service.deleteById(id); - propagationService.propagate(entity, DELETE, context.getTenantId()); + propagationService.propagate(getPropagationData(entity, null), DELETE, context.getTenantId()); } public AuthoritySourceFileHridDto getAuthoritySourceFileNextHrid(UUID id) { @@ -126,9 +135,9 @@ private void validateActionRightsForTenant(DomainEventType action) { } } - private void validatePatchRequest(AuthoritySourceFilePatchDto patchDto, AuthoritySourceFile existing) { + private void validatePatchRequest(AuthoritySourceFilePatchDto patchDto, AuthoritySourceFile existing, + boolean hasAuthorityReferences) { var errorParameters = new LinkedList(); - var hasAuthorityReferences = anyAuthoritiesExistForSourceFile(existing); if (!(existing.getSource().equals(FOLIO) || hasAuthorityReferences)) { return; @@ -167,4 +176,18 @@ public boolean anyAuthoritiesExistForSourceFile(AuthoritySourceFile sourceFile) return false; } + + private AuthoritySourceFilePropagationData getPropagationData( + AuthoritySourceFile authoritySourceFile, BiConsumer publishConsumer) { + return new AuthoritySourceFilePropagationData<>(authoritySourceFile, publishConsumer); + } + + @NotNull + private BiConsumer getUpdatePublishConsumer() { + return (newAsf, oldAsf) -> eventPublisher.publishUpdateEvent(mapper.toDto(newAsf), mapper.toDto(oldAsf)); + } + + private boolean publishRequired(boolean hasRefs, AuthoritySourceFilePatchDto modified, AuthoritySourceFile existed) { + return hasRefs && !modified.getBaseUrl().equals(existed.getBaseUrl()); + } } diff --git a/src/main/java/org/folio/entlinks/service/authority/AuthorityDomainEventPublisher.java b/src/main/java/org/folio/entlinks/service/authority/AuthorityDomainEventPublisher.java index 7c31e7ef..586075a0 100644 --- a/src/main/java/org/folio/entlinks/service/authority/AuthorityDomainEventPublisher.java +++ b/src/main/java/org/folio/entlinks/service/authority/AuthorityDomainEventPublisher.java @@ -9,6 +9,7 @@ import org.folio.entlinks.integration.kafka.EventProducer; import org.folio.entlinks.service.reindex.ReindexContext; import org.folio.spring.FolioExecutionContext; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component @@ -19,6 +20,7 @@ public class AuthorityDomainEventPublisher { private static final String DOMAIN_EVENT_TYPE_HEADER = "domain-event-type"; private static final String REINDEX_JOB_ID_HEADER = "reindex-job-id"; + @Qualifier("authorityDomainMessageProducer") private final EventProducer> eventProducer; private final FolioExecutionContext folioExecutionContext; diff --git a/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisher.java b/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisher.java new file mode 100644 index 00000000..15aa9b50 --- /dev/null +++ b/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisher.java @@ -0,0 +1,37 @@ +package org.folio.entlinks.service.authority; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.entlinks.domain.dto.AuthoritySourceFileDto; +import org.folio.entlinks.integration.dto.event.DomainEvent; +import org.folio.entlinks.integration.dto.event.DomainEventType; +import org.folio.entlinks.integration.kafka.EventProducer; +import org.folio.spring.FolioExecutionContext; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class AuthoritySourceFileDomainEventPublisher { + private static final String DOMAIN_EVENT_TYPE_HEADER = "domain-event-type"; + + @Qualifier("authoritySourceFileDomainMessageProducer") + private final EventProducer> eventProducer; + private final FolioExecutionContext folioExecutionContext; + + public void publishUpdateEvent(AuthoritySourceFileDto oldAsfDto, AuthoritySourceFileDto updatedAsfDto) { + var id = updatedAsfDto.getId(); + if (id == null || oldAsfDto.getId() == null) { + log.warn("Old/New Authority Source File cannot have null id: updated.id - {}, old.id: {}", + id, oldAsfDto.getId()); + return; + } + + log.debug("publishUpdated::process authority source file id={}", id); + + var domainEvent = DomainEvent.updateEvent(id, oldAsfDto, updatedAsfDto, + folioExecutionContext.getTenantId()); + eventProducer.sendMessage(id.toString(), domainEvent, DOMAIN_EVENT_TYPE_HEADER, DomainEventType.UPDATE); + } +} diff --git a/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileService.java b/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileService.java index 4cca5891..0f3a1961 100644 --- a/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileService.java +++ b/src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileService.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.function.BiConsumer; import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; @@ -120,20 +121,17 @@ public AuthoritySourceFile create(AuthoritySourceFile entity) { maxAttempts = 2, backoff = @Backoff(delay = 500)) public AuthoritySourceFile update(UUID id, AuthoritySourceFile modified) { - log.debug("update:: Attempting to update AuthoritySourceFile [id: {}]", id); - - validateOnUpdate(id, modified); - - var existingEntity = repository.findById(id).orElseThrow(() -> new AuthoritySourceFileNotFoundException(id)); - if (modified.getVersion() < existingEntity.getVersion()) { - throw OptimisticLockingException.optimisticLockingOnUpdate( - id, existingEntity.getVersion(), modified.getVersion()); - } - - updateSequenceStartNumber(existingEntity, modified); + return updateInner(id, modified, null); + } - copyModifiableFields(existingEntity, modified); - return repository.save(existingEntity); + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Retryable( + retryFor = OptimisticLockingException.class, + maxAttempts = 2, + backoff = @Backoff(delay = 500)) + public AuthoritySourceFile update(UUID id, AuthoritySourceFile modified, + BiConsumer publishConsumer) { + return updateInner(id, modified, publishConsumer); } public void deleteById(UUID id) { @@ -182,6 +180,30 @@ private void validateOnCreate(AuthoritySourceFile entity) { entity.getAuthoritySourceFileCodes().forEach(this::validateSourceFileCode); } + private AuthoritySourceFile updateInner(UUID id, AuthoritySourceFile modified, + BiConsumer publishConsumer) { + log.debug("update:: Attempting to update AuthoritySourceFile [id: {}]", id); + + validateOnUpdate(id, modified); + + var existingEntity = repository.findById(id).orElseThrow(() -> new AuthoritySourceFileNotFoundException(id)); + var detachedExisting = new AuthoritySourceFile(existingEntity); + if (modified.getVersion() < existingEntity.getVersion()) { + throw OptimisticLockingException.optimisticLockingOnUpdate( + id, existingEntity.getVersion(), modified.getVersion()); + } + + updateSequenceStartNumber(existingEntity, modified); + + copyModifiableFields(existingEntity, modified); + + AuthoritySourceFile saved = repository.saveAndFlush(existingEntity); + if (publishConsumer != null) { + publishConsumer.accept(saved, detachedExisting); + } + return saved; + } + private void validateOnUpdate(UUID id, AuthoritySourceFile entity) { if (!Objects.equals(id, entity.getId())) { throw new RequestBodyValidationException("Request should have id = " + id, diff --git a/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumAuthoritySourceFilePropagationService.java b/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumAuthoritySourceFilePropagationService.java index 0d6d9f41..57ca27bd 100644 --- a/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumAuthoritySourceFilePropagationService.java +++ b/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumAuthoritySourceFilePropagationService.java @@ -4,12 +4,14 @@ import org.folio.entlinks.domain.entity.AuthoritySourceFile; import org.folio.entlinks.service.authority.AuthoritySourceFileService; import org.folio.entlinks.service.consortium.ConsortiumTenantsService; +import org.folio.entlinks.service.consortium.propagation.model.AuthoritySourceFilePropagationData; import org.folio.spring.service.SystemUserScopedExecutionService; import org.springframework.stereotype.Service; @Log4j2 @Service -public class ConsortiumAuthoritySourceFilePropagationService extends ConsortiumPropagationService { +public class ConsortiumAuthoritySourceFilePropagationService + extends ConsortiumPropagationService> { private final AuthoritySourceFileService sourceFileService; @@ -20,13 +22,14 @@ public ConsortiumAuthoritySourceFilePropagationService(AuthoritySourceFileServic this.sourceFileService = sourceFileService; } - protected void doPropagation(AuthoritySourceFile sourceFile, PropagationType propagationType) { + protected void doPropagation(AuthoritySourceFilePropagationData data, + PropagationType propagationType) { + var sourceFile = data.authoritySourceFile(); switch (propagationType) { case CREATE -> sourceFileService.create(sourceFile); - case UPDATE -> sourceFileService.update(sourceFile.getId(), sourceFile); + case UPDATE -> sourceFileService.update(sourceFile.getId(), sourceFile, data.publishConsumer()); case DELETE -> sourceFileService.deleteById(sourceFile.getId()); default -> throw new IllegalStateException("Unexpected value: " + propagationType); } } - } diff --git a/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumPropagationService.java b/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumPropagationService.java index 3ef19c19..7294640a 100644 --- a/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumPropagationService.java +++ b/src/main/java/org/folio/entlinks/service/consortium/propagation/ConsortiumPropagationService.java @@ -34,7 +34,6 @@ public void propagate(T entity, PropagationType propagationType, } catch (FolioIntegrationException e) { log.warn("Skip propagation. Exception: ", e); } - } protected abstract void doPropagation(T entity, diff --git a/src/main/java/org/folio/entlinks/service/consortium/propagation/model/AuthoritySourceFilePropagationData.java b/src/main/java/org/folio/entlinks/service/consortium/propagation/model/AuthoritySourceFilePropagationData.java new file mode 100644 index 00000000..a0da0cd3 --- /dev/null +++ b/src/main/java/org/folio/entlinks/service/consortium/propagation/model/AuthoritySourceFilePropagationData.java @@ -0,0 +1,8 @@ +package org.folio.entlinks.service.consortium.propagation.model; + +import java.util.function.BiConsumer; +import org.folio.entlinks.domain.entity.AuthoritySourceFile; + +public record AuthoritySourceFilePropagationData(AuthoritySourceFile authoritySourceFile, + BiConsumer publishConsumer) { +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b5f7ece8..08695ca2 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -74,6 +74,9 @@ folio: permissionsFilePath: permissions/mod-entities-links-permissions.csv kafka: topics: + - name: authority.authority-source-file + numPartitions: ${KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_PARTITIONS:1} + replicationFactor: ${KAFKA_AUTHORITY_SOURCE_FILE_TOPIC_REPLICATION_FACTOR:} - name: authorities.authority numPartitions: ${KAFKA_INSTANCE_AUTHORITY_TOPIC_PARTITIONS:50} replicationFactor: ${KAFKA_INSTANCE_AUTHORITY_TOPIC_REPLICATION_FACTOR:} diff --git a/src/test/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegateTest.java b/src/test/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegateTest.java index 0a430d2e..e0081e49 100644 --- a/src/test/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegateTest.java +++ b/src/test/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegateTest.java @@ -22,6 +22,8 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.stream.Stream; import org.folio.entlinks.controller.converter.AuthoritySourceFileMapper; import org.folio.entlinks.domain.dto.AuthoritySourceFileDto; @@ -34,10 +36,12 @@ import org.folio.entlinks.domain.entity.AuthoritySourceFile; import org.folio.entlinks.domain.entity.AuthoritySourceFileSource; import org.folio.entlinks.exception.RequestBodyValidationException; +import org.folio.entlinks.service.authority.AuthoritySourceFileDomainEventPublisher; import org.folio.entlinks.service.authority.AuthoritySourceFileService; import org.folio.entlinks.service.consortium.ConsortiumTenantsService; import org.folio.entlinks.service.consortium.UserTenantsService; import org.folio.entlinks.service.consortium.propagation.ConsortiumAuthoritySourceFilePropagationService; +import org.folio.entlinks.service.consortium.propagation.model.AuthoritySourceFilePropagationData; import org.folio.spring.FolioExecutionContext; import org.folio.spring.service.SystemUserScopedExecutionService; import org.folio.spring.testing.type.UnitTest; @@ -75,6 +79,8 @@ class AuthoritySourceFileServiceDelegateTest { private SystemUserScopedExecutionService executionService; @Mock private ConsortiumTenantsService consortiumTenantsService; + @Mock + private AuthoritySourceFileDomainEventPublisher eventPublisher; @InjectMocks private AuthoritySourceFileServiceDelegate delegate; @@ -128,7 +134,7 @@ void shouldNormalizeBaseUrlForSourceFileCreate() { verify(service).create(expected); verify(mapper).toDto(expected); verify(service).createSequence(expected.getSequenceName(), expected.getHridStartNumber()); - verify(propagationService).propagate(expected, CREATE, TENANT_ID); + verify(propagationService).propagate(getMockData(expected, null), CREATE, TENANT_ID); verifyNoMoreInteractions(mapper, service); } @@ -153,7 +159,7 @@ void shouldNormalizeBaseUrlForSourceFileCreateOnConsortiumCentralTenant() { verify(service).create(expected); verify(mapper).toDto(expected); verify(service).createSequence(expected.getSequenceName(), expected.getHridStartNumber()); - verify(propagationService).propagate(expected, CREATE, CENTRAL_TENANT_ID); + verify(propagationService).propagate(getMockData(expected, null), CREATE, CENTRAL_TENANT_ID); verifyNoMoreInteractions(mapper, service); } @@ -170,18 +176,83 @@ void shouldNormalizeBaseUrlForSourceFilePartialUpdate() { when(service.authoritiesExistForSourceFile(existing.getId())).thenReturn(true); when(mapper.partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class))) .thenAnswer(i -> i.getArguments()[1]); - when(service.update(any(UUID.class), any(AuthoritySourceFile.class))).thenAnswer(i -> i.getArguments()[1]); + when(service.update(any(UUID.class), any(AuthoritySourceFile.class), any())).thenAnswer(i -> i.getArguments()[1]); + var dto = new AuthoritySourceFilePatchDto().baseUrl(INPUT_BASE_URL); + + delegate.patchAuthoritySourceFile(existing.getId(), dto); + + verify(service).update(eq(existing.getId()), sourceFileArgumentCaptor.capture(), eq(null)); + var patchedSourceFile = sourceFileArgumentCaptor.getValue(); + assertThat(expected).usingDefaultComparator().isEqualTo(patchedSourceFile); + verify(service).authoritiesExistForSourceFile(existing.getId()); + verify(mapper).partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class)); + verify(service).getById(any(UUID.class)); + verify(propagationService).propagate(getMockData(expected, null), UPDATE, TENANT_ID); + verifyNoMoreInteractions(mapper, service); + } + + @Test + void shouldNotProduceEventForSourceFilePartialUpdate() { + var existing = TestDataUtils.AuthorityTestData.authoritySourceFile(0); + existing.setBaseUrl(INPUT_BASE_URL); + existing.setSource(AuthoritySourceFileSource.FOLIO); + var expected = new AuthoritySourceFile(existing); + expected.setBaseUrl(SANITIZED_BASE_URL); + + mockAsNonConsortiumTenant(); + when(service.getById(existing.getId())).thenReturn(existing); + when(service.authoritiesExistForSourceFile(existing.getId())).thenReturn(true); + when(mapper.partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class))) + .thenAnswer(i -> i.getArguments()[1]); + when(service.update(any(UUID.class), any(AuthoritySourceFile.class), eq(null))).thenReturn(expected); var dto = new AuthoritySourceFilePatchDto().baseUrl(INPUT_BASE_URL); delegate.patchAuthoritySourceFile(existing.getId(), dto); - verify(service).update(eq(existing.getId()), sourceFileArgumentCaptor.capture()); + verify(service).update(eq(existing.getId()), sourceFileArgumentCaptor.capture(), any()); var patchedSourceFile = sourceFileArgumentCaptor.getValue(); assertThat(expected).usingDefaultComparator().isEqualTo(patchedSourceFile); verify(service).authoritiesExistForSourceFile(existing.getId()); verify(mapper).partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class)); verify(service).getById(any(UUID.class)); - verify(propagationService).propagate(expected, UPDATE, TENANT_ID); + verify(propagationService).propagate(getMockData(expected, null), UPDATE, TENANT_ID); + verifyNoMoreInteractions(mapper, service); + } + + @Test + void shouldProduceEventForSourceFilePartialUpdate() { + var changeUrlSuffix = "change/suffix"; + var existing = TestDataUtils.AuthorityTestData.authoritySourceFile(0); + existing.setBaseUrl(INPUT_BASE_URL); + existing.setSource(AuthoritySourceFileSource.FOLIO); + var expected = new AuthoritySourceFile(existing); + expected.setBaseUrl(SANITIZED_BASE_URL + changeUrlSuffix); + + mockAsConsortiumCentralTenant(); + when(service.getById(existing.getId())).thenReturn(existing); + when(service.authoritiesExistForSourceFile(existing.getId())).thenReturn(true); + when(mapper.partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class))) + .thenAnswer(i -> i.getArguments()[1]); + var existingDto = new AuthoritySourceFileDto().id(existing.getId()); + var modifiedDto = new AuthoritySourceFileDto().id(existing.getId()); + AtomicReference> consumer = new AtomicReference<>(); + when(mapper.toDto(any(AuthoritySourceFile.class))).thenReturn(modifiedDto).thenReturn(existingDto); + when(service.update(any(UUID.class), any(AuthoritySourceFile.class), any())).thenAnswer(invocation -> { + consumer.set(invocation.getArgument(2)); + consumer.get().accept(expected, existing); + return expected; + }); + var dto = new AuthoritySourceFilePatchDto().baseUrl(INPUT_BASE_URL + changeUrlSuffix); + + delegate.patchAuthoritySourceFile(existing.getId(), dto); + + verify(service).update(eq(existing.getId()), sourceFileArgumentCaptor.capture(), any()); + var patchedSourceFile = sourceFileArgumentCaptor.getValue(); + assertThat(expected).usingDefaultComparator().isEqualTo(patchedSourceFile); + verify(service).authoritiesExistForSourceFile(existing.getId()); + verify(mapper).partialUpdate(any(AuthoritySourceFilePatchDto.class), any(AuthoritySourceFile.class)); + verify(service).getById(any(UUID.class)); + verify(propagationService).propagate(getMockData(expected, consumer.get()), UPDATE, CENTRAL_TENANT_ID); verifyNoMoreInteractions(mapper, service); } @@ -256,7 +327,7 @@ void shouldDeleteAuthoritySourceFileById() { verify(service).deleteSequence(existing.getSequenceName()); verify(service).deleteById(existing.getId()); - verify(propagationService).propagate(existing, DELETE, TENANT_ID); + verify(propagationService).propagate(getMockData(existing, null), DELETE, TENANT_ID); } @Test @@ -398,4 +469,9 @@ private void mockAsConsortiumMemberTenant() { when(context.getTenantId()).thenReturn(TENANT_ID); when(tenantsService.getCentralTenant(TENANT_ID)).thenReturn(Optional.of(CENTRAL_TENANT_ID)); } + + private AuthoritySourceFilePropagationData getMockData( + AuthoritySourceFile asf, BiConsumer consumer) { + return new AuthoritySourceFilePropagationData<>(asf, consumer); + } } diff --git a/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisherTest.java b/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisherTest.java new file mode 100644 index 00000000..6361b94b --- /dev/null +++ b/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileDomainEventPublisherTest.java @@ -0,0 +1,67 @@ +package org.folio.entlinks.service.authority; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.UUID; +import org.folio.entlinks.domain.dto.AuthoritySourceFileDto; +import org.folio.entlinks.integration.dto.event.DomainEvent; +import org.folio.entlinks.integration.dto.event.DomainEventType; +import org.folio.entlinks.integration.kafka.EventProducer; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class AuthoritySourceFileDomainEventPublisherTest { + private static final String TENANT_ID = "test"; + private static final String DOMAIN_EVENT_TYPE_HEADER = "domain-event-type"; + + @Mock + private EventProducer> eventProducer; + + @Mock + private FolioExecutionContext folioExecutionContext; + + @InjectMocks + private AuthoritySourceFileDomainEventPublisher eventPublisher; + + private final ArgumentCaptor captor = ArgumentCaptor.forClass(DomainEvent.class); + + @Test + void shouldNotSendUpdatedEventWhenIdIsNull() { + // when + eventPublisher.publishUpdateEvent(new AuthoritySourceFileDto(), new AuthoritySourceFileDto()); + + // then + verifyNoInteractions(eventProducer); + } + + @Test + void shouldSendUpdatedEvent() { + // given + var id = UUID.randomUUID(); + var oldDto = new AuthoritySourceFileDto().id(id).source(AuthoritySourceFileDto.SourceEnum.FOLIO); + var newDto = new AuthoritySourceFileDto().id(id).source(AuthoritySourceFileDto.SourceEnum.FOLIO); + when(folioExecutionContext.getTenantId()).thenReturn(TENANT_ID); + + // when + eventPublisher.publishUpdateEvent(oldDto, newDto); + + // then + verify(eventProducer).sendMessage(eq(oldDto.getId().toString()), captor.capture(), eq(DOMAIN_EVENT_TYPE_HEADER), + eq(DomainEventType.UPDATE)); + assertEquals(newDto, captor.getValue().getNewEntity()); + assertEquals(oldDto, captor.getValue().getOldEntity()); + assertEquals(TENANT_ID, captor.getValue().getTenant()); + } +} diff --git a/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileServiceTest.java b/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileServiceTest.java index 90df72b8..2ddef98d 100644 --- a/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileServiceTest.java +++ b/src/test/java/org/folio/entlinks/service/authority/AuthoritySourceFileServiceTest.java @@ -210,7 +210,7 @@ void shouldUpdateAuthoritySourceFileModifiableFields(Integer existingHridStartNu when(repository.findById(id)).thenReturn(Optional.of(existing)); when(moduleMetadata.getDBSchemaName(any())).thenReturn("test"); - when(repository.save(expected)).thenReturn(expected); + when(repository.saveAndFlush(expected)).thenReturn(expected); when(mapper.toDtoCodes(existing.getAuthoritySourceFileCodes())).thenReturn(existingDtoCodes); when(mapper.toDtoCodes(modified.getAuthoritySourceFileCodes())).thenReturn(modifiedDtoCodes); @@ -218,7 +218,7 @@ void shouldUpdateAuthoritySourceFileModifiableFields(Integer existingHridStartNu assertThat(actual).isEqualTo(expected); verify(repository).findById(id); - verify(repository).save(argThat(authoritySourceFileMatch(expected))); + verify(repository).saveAndFlush(argThat(authoritySourceFileMatch(expected))); var argumentCaptor = ArgumentCaptor.forClass(String.class); verify(jdbcTemplate, times(2)).execute(argumentCaptor.capture()); @@ -255,7 +255,7 @@ void shouldUpdateAuthoritySourceFile_WhenSequenceStartNumberLessThenExisting(Int when(repository.findById(id)).thenReturn(Optional.of(existing)); when(moduleMetadata.getDBSchemaName(any())).thenReturn("test"); - when(repository.save(expected)).thenReturn(expected); + when(repository.saveAndFlush(expected)).thenReturn(expected); when(mapper.toDtoCodes(existing.getAuthoritySourceFileCodes())).thenReturn(existingDtoCodes); when(mapper.toDtoCodes(modified.getAuthoritySourceFileCodes())).thenReturn(modifiedDtoCodes); @@ -263,7 +263,7 @@ void shouldUpdateAuthoritySourceFile_WhenSequenceStartNumberLessThenExisting(Int assertThat(actual).isEqualTo(expected); verify(repository).findById(id); - verify(repository).save(argThat(authoritySourceFileMatch(expected))); + verify(repository).saveAndFlush(argThat(authoritySourceFileMatch(expected))); var argumentCaptor = ArgumentCaptor.forClass(String.class); verify(jdbcTemplate, times(2)).execute(argumentCaptor.capture()); @@ -289,7 +289,7 @@ void shouldUpdateAuthoritySource_NotUpdatingSequenceStartNumberWhenNotChanged() var expected = new AuthoritySourceFile(modified); when(repository.findById(id)).thenReturn(Optional.of(existing)); - when(repository.save(expected)).thenReturn(expected); + when(repository.saveAndFlush(expected)).thenReturn(expected); when(mapper.toDtoCodes(existing.getAuthoritySourceFileCodes())).thenReturn(emptyList()); when(mapper.toDtoCodes(modified.getAuthoritySourceFileCodes())).thenReturn(emptyList()); @@ -297,7 +297,7 @@ void shouldUpdateAuthoritySource_NotUpdatingSequenceStartNumberWhenNotChanged() assertThat(actual).isEqualTo(expected); verify(repository).findById(id); - verify(repository).save(expected); + verify(repository).saveAndFlush(expected); verifyNoInteractions(context); verifyNoInteractions(moduleMetadata); verifyNoInteractions(jdbcTemplate); diff --git a/src/test/java/org/folio/entlinks/service/consortium/ConsortiumAuthoritySourceFilePropagationServiceTest.java b/src/test/java/org/folio/entlinks/service/consortium/ConsortiumAuthoritySourceFilePropagationServiceTest.java index 4f0a6cf7..7937aaa5 100644 --- a/src/test/java/org/folio/entlinks/service/consortium/ConsortiumAuthoritySourceFilePropagationServiceTest.java +++ b/src/test/java/org/folio/entlinks/service/consortium/ConsortiumAuthoritySourceFilePropagationServiceTest.java @@ -16,6 +16,7 @@ import org.folio.entlinks.service.authority.AuthoritySourceFileService; import org.folio.entlinks.service.consortium.propagation.ConsortiumAuthorityPropagationService; import org.folio.entlinks.service.consortium.propagation.ConsortiumAuthoritySourceFilePropagationService; +import org.folio.entlinks.service.consortium.propagation.model.AuthoritySourceFilePropagationData; import org.folio.spring.service.SystemUserScopedExecutionService; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; @@ -39,7 +40,8 @@ void testPropagateCreate() throws FolioIntegrationException { var sourceFile = new AuthoritySourceFile(); sourceFile.setSource(LOCAL); doMocks(); - propagationService.propagate(sourceFile, ConsortiumAuthorityPropagationService.PropagationType.CREATE, TENANT_ID); + propagationService.propagate(getMockData(sourceFile), + ConsortiumAuthorityPropagationService.PropagationType.CREATE, TENANT_ID); assertThat(sourceFile.getSource()).isEqualTo(LOCAL); verify(tenantsService).getConsortiumTenants(TENANT_ID); @@ -53,12 +55,13 @@ void testPropagateUpdate() throws FolioIntegrationException { sourceFile.setId(UUID.randomUUID()); sourceFile.setSource(LOCAL); doMocks(); - propagationService.propagate(sourceFile, ConsortiumAuthorityPropagationService.PropagationType.UPDATE, TENANT_ID); + propagationService.propagate(getMockData(sourceFile), + ConsortiumAuthorityPropagationService.PropagationType.UPDATE, TENANT_ID); assertThat(sourceFile.getSource()).isEqualTo(LOCAL); verify(tenantsService, times(1)).getConsortiumTenants(any()); verify(executionService, times(3)).executeAsyncSystemUserScoped(any(), any()); - verify(authorityService, times(3)).update(sourceFile.getId(), sourceFile); + verify(authorityService, times(3)).update(sourceFile.getId(), sourceFile, null); } @Test @@ -67,7 +70,8 @@ void testPropagateDelete() throws FolioIntegrationException { sourceFile.setId(UUID.randomUUID()); sourceFile.setSource(LOCAL); doMocks(); - propagationService.propagate(sourceFile, ConsortiumAuthorityPropagationService.PropagationType.DELETE, TENANT_ID); + propagationService.propagate(getMockData(sourceFile), + ConsortiumAuthorityPropagationService.PropagationType.DELETE, TENANT_ID); assertThat(sourceFile.getSource()).isEqualTo(LOCAL); verify(tenantsService, times(1)).getConsortiumTenants(any()); @@ -80,7 +84,8 @@ void testPropagateException() throws FolioIntegrationException { Mockito.doThrow(FolioIntegrationException.class).when(tenantsService).getConsortiumTenants(any()); var sourceFile = new AuthoritySourceFile(); - propagationService.propagate(sourceFile, ConsortiumAuthorityPropagationService.PropagationType.CREATE, TENANT_ID); + propagationService.propagate(getMockData(sourceFile), + ConsortiumAuthorityPropagationService.PropagationType.CREATE, TENANT_ID); verify(tenantsService, times(1)).getConsortiumTenants(any()); verify(executionService, times(0)).executeAsyncSystemUserScoped(any(), any()); @@ -95,4 +100,7 @@ private void doMocks() { }).when(executionService).executeAsyncSystemUserScoped(any(), any()); } + private AuthoritySourceFilePropagationData getMockData(AuthoritySourceFile asf) { + return new AuthoritySourceFilePropagationData<>(asf, null); + } } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index be0af8ca..675ba3da 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -54,6 +54,9 @@ folio: - name: authorities.authority numPartitions: 1 replicationFactor: + - name: authority.authority-source-file + numPartitions: 1 + replicationFactor: listener: authority: concurrency: 1