From 2ca7390b390e469ef267d54468c533f6608a7469 Mon Sep 17 00:00:00 2001 From: saba_zedginidze Date: Thu, 30 Jul 2026 15:30:48 +0400 Subject: [PATCH 1/6] [MODEXPS-315] Export Configuration create/update domain events --- NEWS.md | 5 + .../des/config/ServiceConfiguration.java | 28 +++- .../folio/des/config/kafka/KafkaService.java | 14 +- .../des/domain/dto/event/DomainEvent.java | 31 +++++ .../des/domain/dto/event/DomainEventType.java | 7 + .../ExportConfigDomainEventService.java | 71 ++++++++++ .../config/ExportConfigEventProducer.java | 47 +++++++ .../acquisition/ClaimsExportService.java | 9 +- .../EdifactOrdersExportService.java | 7 +- .../config/impl/BaseExportConfigService.java | 35 ++++- .../BursarFeesFinesExportConfigService.java | 6 +- src/main/resources/application.yml | 4 + .../des/controller/ConfigsControllerTest.java | 55 +++++++- .../acquisition/ClaimsExportServiceTest.java | 7 +- .../EdifactOrdersExportServiceTest.java | 6 +- .../impl/BaseExportConfigServiceTest.java | 6 +- ...ursarFeesFinesExportConfigServiceTest.java | 7 +- .../folio/des/support/TestKafkaConsumer.java | 128 ++++++++++++++++++ 18 files changed, 449 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/folio/des/domain/dto/event/DomainEvent.java create mode 100644 src/main/java/org/folio/des/domain/dto/event/DomainEventType.java create mode 100644 src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java create mode 100644 src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java create mode 100644 src/test/java/org/folio/des/support/TestKafkaConsumer.java diff --git a/NEWS.md b/NEWS.md index f6bd8b7d..51fc86ed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## 2026-07-28 v3.7.0 + +### Stories +* Publish Kafka domain events (CREATE/UPDATE) for Export Configuration changes on `folio.ALL.data-export-spring.config` with structural credential redaction + ## 2026-05-19 v3.6.1 [Full Changelog](https://github.com/folio-org/mod-data-export-spring/compare/v3.6.0...v3.6.1) diff --git a/src/main/java/org/folio/des/config/ServiceConfiguration.java b/src/main/java/org/folio/des/config/ServiceConfiguration.java index 42f037c0..1471eae5 100644 --- a/src/main/java/org/folio/des/config/ServiceConfiguration.java +++ b/src/main/java/org/folio/des/config/ServiceConfiguration.java @@ -28,6 +28,7 @@ import org.folio.des.scheduling.quartz.converter.acquisition.ExportConfigToEdifactJobDetailConverter; import org.folio.des.scheduling.quartz.converter.acquisition.ExportConfigToEdifactTriggerConverter; import org.folio.des.scheduling.quartz.job.acquisition.EdifactJobKeyResolver; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.service.config.ExportConfigService; import org.folio.des.service.config.acquisition.ClaimsExportService; import org.folio.des.service.config.acquisition.EdifactOrdersExportService; @@ -40,11 +41,14 @@ import org.folio.des.validator.acquisition.ClaimsExportParametersValidator; import org.folio.des.validator.acquisition.EdifactOrdersExportParametersValidator; import org.quartz.Scheduler; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.validation.Validator; +import com.fasterxml.jackson.databind.ObjectMapper; + @Configuration @ComponentScan("org.folio.des") public class ServiceConfiguration { @@ -82,8 +86,11 @@ BursarFeesFinesExportConfigService bursarExportConfigService(ExportConfigReposit DefaultExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, + @Qualifier("entityObjectMapper") ObjectMapper objectMapper, BursarExportScheduler bursarExportScheduler) { - return new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, bursarExportScheduler); + return new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, + exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper, bursarExportScheduler); } @Bean @@ -91,24 +98,33 @@ EdifactOrdersExportService edifactOrdersExportService(ExportConfigRepository rep EdifactExportConfigMapper edifactExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, + @Qualifier("entityObjectMapper") ObjectMapper objectMapper, ExportJobScheduler exportJobScheduler) { - return new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportJobScheduler); + return new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, + exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper, exportJobScheduler); } @Bean ClaimsExportService claimsExportService(ExportConfigRepository repository, ClaimsExportConfigMapper claimsExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, - ExportConfigValidatorResolver exportConfigValidatorResolver) { - return new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, + @Qualifier("entityObjectMapper") ObjectMapper objectMapper) { + return new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, + exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); } @Bean BaseExportConfigService defaultExportConfigService(ExportConfigRepository repository, DefaultExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, - ExportConfigValidatorResolver exportConfigValidatorResolver) { - return new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, + @Qualifier("entityObjectMapper") ObjectMapper objectMapper) { + return new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, + exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); } @Bean diff --git a/src/main/java/org/folio/des/config/kafka/KafkaService.java b/src/main/java/org/folio/des/config/kafka/KafkaService.java index bde58748..277d501a 100644 --- a/src/main/java/org/folio/des/config/kafka/KafkaService.java +++ b/src/main/java/org/folio/des/config/kafka/KafkaService.java @@ -41,7 +41,8 @@ public class KafkaService { @RequiredArgsConstructor @Getter public enum Topic { - JOB_COMMAND("data-export.job.command"); + JOB_COMMAND("data-export.job.command"), + CONFIG("data-export-spring.config"); private final String topicName; } @@ -90,6 +91,17 @@ private NewTopic toKafkaTopic(String tenant, Topic topic) { * @param tenantId tenant id as {@link String} * @return topic name as {@link String} object */ + /** + * Returns the tenant-scoped topic name for the given {@link Topic} in the format {@code {env}.{tenant}.topicName}. + * + * @param topic the logical topic + * @param tenant the tenant id + * @return the fully-qualified tenant topic name + */ + public String getTenantTopicName(Topic topic, String tenant) { + return getTenantTopicName(topic.getTopicName(), tenant); + } + private String getTenantTopicName(String topicName, String tenantId) { return KafkaUtils.getTenantTopicName(topicName, environment, tenantId); } diff --git a/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java b/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java new file mode 100644 index 00000000..e3914ac6 --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java @@ -0,0 +1,31 @@ +package org.folio.des.domain.dto.event; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DomainEvent { + + private UUID eventId; + private long eventTs; + private String tenant; + private DomainEventType type; + + @JsonProperty("old") + private T oldValue; + + @JsonProperty("new") + private T newValue; +} + diff --git a/src/main/java/org/folio/des/domain/dto/event/DomainEventType.java b/src/main/java/org/folio/des/domain/dto/event/DomainEventType.java new file mode 100644 index 00000000..fc08aca2 --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/DomainEventType.java @@ -0,0 +1,7 @@ +package org.folio.des.domain.dto.event; + +public enum DomainEventType { + CREATE, + UPDATE +} + diff --git a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java new file mode 100644 index 00000000..5b8c8501 --- /dev/null +++ b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java @@ -0,0 +1,71 @@ +package org.folio.des.service.config; + +import java.util.UUID; + +import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.event.DomainEvent; +import org.folio.des.domain.dto.event.DomainEventType; +import org.folio.spring.FolioExecutionContext; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +/** + * Builds {@link DomainEvent} envelopes for Export Configuration changes and hands them to the + * {@link ExportConfigEventProducer}. Config services delegate here so they never build events directly. + */ +@Log4j2 +@Service +@RequiredArgsConstructor +public class ExportConfigDomainEventService { + + private final ExportConfigEventProducer exportConfigEventProducer; + private final FolioExecutionContext folioExecutionContext; + + /** + * Publishes a {@code CREATE} Export Configuration event carrying the new snapshot. + * + * @param newConfig the newly-created, sanitized configuration snapshot + */ + public void publishConfigCreatedEvent(ExportConfig newConfig) { + publish(newConfig.getId(), createEvent(newConfig)); + } + + /** + * Publishes an {@code UPDATE} Export Configuration event carrying both the pre- and post-change snapshots. + * + * @param oldConfig the pre-change, sanitized configuration snapshot + * @param newConfig the post-change, sanitized configuration snapshot + */ + public void publishConfigUpdatedEvent(ExportConfig oldConfig, ExportConfig newConfig) { + publish(newConfig.getId(), updateEvent(oldConfig, newConfig)); + } + + private DomainEvent createEvent(ExportConfig newConfig) { + return DomainEvent.builder() + .eventId(UUID.randomUUID()) + .eventTs(System.currentTimeMillis()) + .tenant(folioExecutionContext.getTenantId()) + .type(DomainEventType.CREATE) + .newValue(newConfig) + .build(); + } + + private DomainEvent updateEvent(ExportConfig oldConfig, ExportConfig newConfig) { + return DomainEvent.builder() + .eventId(UUID.randomUUID()) + .eventTs(System.currentTimeMillis()) + .tenant(folioExecutionContext.getTenantId()) + .type(DomainEventType.UPDATE) + .oldValue(oldConfig) + .newValue(newConfig) + .build(); + } + + private void publish(String configId, DomainEvent event) { + log.debug("publish:: publishing config event [id: {}, type: {}, tenant: {}]", + configId, event.getType(), event.getTenant()); + exportConfigEventProducer.publish(configId, event); + } +} diff --git a/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java new file mode 100644 index 00000000..574c38ba --- /dev/null +++ b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java @@ -0,0 +1,47 @@ +package org.folio.des.service.config; + +import org.folio.des.config.kafka.KafkaService; +import org.folio.des.config.kafka.KafkaService.Topic; +import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.event.DomainEvent; +import org.folio.spring.FolioExecutionContext; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +/** + * Publishes Export Configuration {@link DomainEvent}s to the tenant-scoped Kafka topic. + * + *

The topic ({@code {env}.{tenant}.data-export-spring.config}) is created on tenant enable by the tenant service + * via {@link KafkaService}. Publishing failures are caught and logged at ERROR — they never fail the originating + * REST request or roll back the DB transaction.

+ */ +@Component +@Log4j2 +@RequiredArgsConstructor +public class ExportConfigEventProducer { + + private final KafkaTemplate kafkaTemplate; + private final KafkaService kafkaService; + private final FolioExecutionContext folioExecutionContext; + + /** + * Publishes an Export Configuration domain event to the tenant-scoped topic. + * + * @param configId the export configuration id, used as the Kafka record key + * @param event the domain event envelope to publish + */ + public void publish(String configId, DomainEvent event) { + var topic = kafkaService.getTenantTopicName(Topic.CONFIG, folioExecutionContext.getTenantId()); + try { + log.info("publish:: Publishing {} event for config id={} on topic={}", event.getType(), configId, topic); + kafkaTemplate.send(topic, configId, event); + log.info("publish:: Successfully published {} event for config id={}", event.getType(), configId); + } catch (Exception e) { + log.error("publish:: Failed to publish {} event for config id={} on topic={}", event.getType(), configId, topic, e); + } + } +} + diff --git a/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java b/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java index c6acdf78..67f8904a 100644 --- a/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java +++ b/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java @@ -3,14 +3,16 @@ import lombok.extern.log4j.Log4j2; import org.folio.des.mapper.BaseExportConfigMapper; -import org.folio.des.mapper.DefaultExportConfigMapper; import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.repository.ExportConfigRepository; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.service.config.impl.BaseExportConfigService; import org.folio.des.validator.ExportConfigValidatorResolver; +import com.fasterxml.jackson.databind.ObjectMapper; + import java.util.Optional; import java.util.UUID; @@ -18,8 +20,9 @@ public class ClaimsExportService extends BaseExportConfigService { public ClaimsExportService(ExportConfigRepository repository, BaseExportConfigMapper defaultExportConfigMapper, - ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper) { + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); } @Override diff --git a/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java b/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java index 93bd10b8..6b6e46e5 100644 --- a/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java +++ b/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java @@ -4,15 +4,17 @@ import java.util.UUID; import org.folio.des.mapper.BaseExportConfigMapper; -import org.folio.des.mapper.DefaultExportConfigMapper; import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.repository.ExportConfigRepository; import org.folio.des.scheduling.ExportJobScheduler; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.service.config.impl.BaseExportConfigService; import org.folio.des.validator.ExportConfigValidatorResolver; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.extern.log4j.Log4j2; @Log4j2 @@ -22,8 +24,9 @@ public class EdifactOrdersExportService extends BaseExportConfigService { public EdifactOrdersExportService(ExportConfigRepository repository, BaseExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper, ExportJobScheduler exportJobScheduler) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); this.exportJobScheduler = exportJobScheduler; } diff --git a/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java b/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java index 56e12e15..4b080a3b 100644 --- a/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java +++ b/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java @@ -7,12 +7,14 @@ import org.folio.de.entity.ExportConfigEntity; import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; import org.folio.des.mapper.BaseExportConfigMapper; import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportConfigCollection; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.repository.ExportConfigRepository; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.service.config.ExportConfigService; import org.folio.des.validator.ExportConfigValidatorResolver; import org.folio.spring.exception.NotFoundException; @@ -21,6 +23,8 @@ import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; @@ -33,17 +37,23 @@ public class BaseExportConfigService implements ExportConfigService { protected final BaseExportConfigMapper exportConfigMapper; protected final ExportConfigMapperResolver exportConfigMapperResolver; protected final ExportConfigValidatorResolver exportConfigValidatorResolver; + protected final ExportConfigDomainEventService exportConfigDomainEventService; + protected final ObjectMapper objectMapper; @Override @Transactional public void updateConfig(String configId, ExportConfig exportConfig) { log.info("updateConfig:: configId={}, exportConfig={}", configId, exportConfig); validateIncomingExportConfig(exportConfig); - getExportConfigEntityOrThrow(configId); + var existingEntity = getExportConfigEntityOrThrow(configId); + var oldSnapshot = sanitize(toDto(existingEntity)); var entity = exportConfigMapper.toEntity(exportConfig); - repository.save(entity); + entity = repository.save(entity); log.info("updateConfig:: Successfully updated config with id={}", configId); + + var newSnapshot = sanitize(toDto(entity)); + exportConfigDomainEventService.publishConfigUpdatedEvent(oldSnapshot, newSnapshot); } @Override @@ -56,6 +66,8 @@ public ExportConfig postConfig(ExportConfig exportConfig) { entity = repository.save(entity); log.info("postConfig:: Successfully created config with id={}", exportConfig.getId()); + exportConfigDomainEventService.publishConfigCreatedEvent(sanitize(toDto(entity))); + return toDto(entity); } @@ -99,6 +111,25 @@ protected ExportConfig toDto(ExportConfigEntity exportConfigEntity) { return exportConfigMapperResolver.resolve(ExportType.fromValue(exportConfigEntity.getType())).toDto(exportConfigEntity); } + /** + * Returns a deep copy of the given configuration with all credential-shaped fields removed, safe to publish on the + * event bus. The original object is left untouched so the REST response still carries the full data. + * + * @param config the configuration snapshot to sanitize + * @return a sanitized deep copy, or {@code null} if the input is {@code null} + */ + protected ExportConfig sanitize(ExportConfig config) { + if (config == null) { + return null; + } + var sanitized = objectMapper.convertValue(config, ExportConfig.class); + Optional.ofNullable(sanitized.getExportTypeSpecificParameters()) + .map(ExportTypeSpecificParameters::getVendorEdiOrdersExportConfig) + .map(VendorEdiOrdersExportConfig::getEdiFtp) + .ifPresent(ediFtp -> ediFtp.setPassword(null)); + return sanitized; + } + protected void validateIncomingExportConfig(ExportConfig exportConfig) { exportConfigValidatorResolver.resolve(exportConfig.getType(), ExportTypeSpecificParameters.class).ifPresent(validator -> { Errors errors = new BeanPropertyBindingResult(exportConfig.getExportTypeSpecificParameters(), "specificParameters"); diff --git a/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java b/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java index c160ad6d..8d2a7b83 100644 --- a/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java +++ b/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java @@ -14,9 +14,12 @@ import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportConfigCollection; import org.folio.des.scheduling.bursar.BursarExportScheduler; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.validator.ExportConfigValidatorResolver; import org.springframework.data.domain.PageRequest; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.extern.log4j.Log4j2; @Log4j2 @@ -26,8 +29,9 @@ public class BursarFeesFinesExportConfigService extends BaseExportConfigService public BursarFeesFinesExportConfigService(ExportConfigRepository repository, DefaultExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, + ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper, BursarExportScheduler bursarExportScheduler) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); this.bursarExportScheduler = bursarExportScheduler; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 37ada618..75cfccf1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,6 +41,10 @@ spring: enabled: true kafka: bootstrap-servers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:9092} + producer: + client-id: mod-data-export-spring + acks: all + retries: 3 datasource: username: ${DB_USERNAME:folio_admin} password: ${DB_PASSWORD:folio_admin} diff --git a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java index e8706700..1d7f0be6 100644 --- a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java +++ b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java @@ -1,7 +1,9 @@ package org.folio.des.controller; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.reset; @@ -16,21 +18,31 @@ import java.util.Objects; +import org.folio.des.config.kafka.KafkaService; +import org.folio.des.config.kafka.KafkaService.Topic; import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.event.DomainEvent; +import org.folio.des.domain.dto.event.DomainEventType; import org.folio.des.repository.ExportConfigRepository; import org.folio.des.scheduling.bursar.BursarExportScheduler; import org.folio.des.support.BaseTest; +import org.folio.des.support.TestKafkaConsumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.kafka.autoconfigure.KafkaProperties; import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.core.type.TypeReference; + +import lombok.SneakyThrows; + @TestPropertySource(properties = "spring.jpa.properties.hibernate.default_schema=diku_mod_data_export_spring") class ConfigsControllerTest extends BaseTest { @@ -47,6 +59,10 @@ class ConfigsControllerTest extends BaseTest { @Autowired private MockMvc mockMvc; + @Autowired + private KafkaService kafkaService; + @Autowired + private KafkaProperties kafkaProperties; @MockitoSpyBean private ExportConfigRepository repository; @MockitoSpyBean @@ -143,6 +159,15 @@ void postConfig() throws Exception { content().contentType("text/plain;charset=UTF-8")); verify(bursarExportScheduler).scheduleBursarJob(any(ExportConfig.class)); + + var event = pollConfigEventJson("0a3cba78-16e7-498e-b75b-98713000277b", DomainEventType.CREATE); + assertThat(event.getType()).isEqualTo(DomainEventType.CREATE); + assertThat(event.getTenant()).isEqualTo(TENANT); + assertNull(event.getOldValue()); + assertThat(event.getNewValue()) + .usingRecursiveComparison() + .ignoringFields("configName", "tenant") + .isEqualTo(OBJECT_MAPPER.readValue(NEW_CONFIG_REQUEST, ExportConfig.class)); } @Test @@ -174,7 +199,7 @@ void postClaimsConfig() throws Exception { @Test @DisplayName("Success update config") void putConfig() throws Exception { - saveConfig(UPDATE_CONFIG_REQUEST); + saveConfig(NEW_CONFIG_REQUEST); mockMvc .perform( @@ -185,6 +210,18 @@ void putConfig() throws Exception { .andExpectAll(status().isNoContent()); verify(bursarExportScheduler).scheduleBursarJob(any(ExportConfig.class)); + + var event = pollConfigEventJson("0a3cba78-16e7-498e-b75b-98713000277b", DomainEventType.UPDATE); + assertThat(event.getType()).isEqualTo(DomainEventType.UPDATE); + assertThat(event.getTenant()).isEqualTo(TENANT); + assertThat(event.getOldValue()) + .usingRecursiveComparison() + .ignoringFields("configName", "tenant") + .isEqualTo(OBJECT_MAPPER.readValue(NEW_CONFIG_REQUEST, ExportConfig.class)); + assertThat(event.getNewValue()) + .usingRecursiveComparison() + .ignoringFields("configName", "tenant") + .isEqualTo(OBJECT_MAPPER.readValue(UPDATE_CONFIG_REQUEST, ExportConfig.class)); } @Test @@ -271,6 +308,22 @@ void shouldNotBeDeletedIfConfigIsNotExist() throws Exception { jsonPath("$.errors[0].message", startsWith("NotFoundException"))); } + private DomainEvent pollConfigEventJson(String configId, DomainEventType type) { + var topic = kafkaService.getTenantTopicName(Topic.CONFIG, TENANT); + try (var consumer = TestKafkaConsumer.subscribe(topic, kafkaProperties)) { + return consumer.poll(configId).stream() + .map(event -> readEvent(event.value())) + .filter(event -> event.getType() == type) + .findFirst() + .orElseThrow(() -> new AssertionError("Expected " + type + " event for config " + configId)); + } + } + + @SneakyThrows + private DomainEvent readEvent(String json) { + return OBJECT_MAPPER.readValue(json, new TypeReference<>() {}); + } + private void saveConfig(String config) throws Exception { mockMvc.perform(post("/data-export-spring/configs") .contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java index 0a50dce5..1d4de751 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java @@ -19,6 +19,7 @@ import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.mapper.acquisition.ClaimsExportConfigMapperImpl; import org.folio.des.repository.ExportConfigRepository; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.validator.ExportConfigValidatorResolver; import org.folio.des.validator.acquisition.ClaimsExportParametersValidator; import org.junit.jupiter.api.BeforeEach; @@ -61,7 +62,8 @@ void setUp() { setInternalState(claimsExportConfigMapper, "validator", validator); repository = Mockito.mock(ExportConfigRepository.class); - service = new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + service = new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, + Mockito.mock(ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper()); } @Test @@ -78,7 +80,8 @@ void testPostConfig() { @DisplayName("Update configuration") void testUpdateConfig() { when(repository.findById(UUID.fromString(CLAIMS_EXPORT_CONFIG.getId()))) - .thenReturn(java.util.Optional.of(new ExportConfigEntity())); + .thenReturn(java.util.Optional.of(new ExportConfigEntity().setType(ExportType.CLAIMS.getValue()))); + when(repository.save(any())).thenAnswer(i -> i.getArguments()[0]); service.updateConfig(CLAIMS_EXPORT_CONFIG.getId(), CLAIMS_EXPORT_CONFIG); diff --git a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java index 41e3713e..bb755aed 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java @@ -74,7 +74,8 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); edifactOrdersExportJobScheduler = Mockito.mock(ExportJobScheduler.class); - service = new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, edifactOrdersExportJobScheduler); + service = new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, + Mockito.mock(org.folio.des.service.config.ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper(), edifactOrdersExportJobScheduler); } @Test @@ -92,7 +93,8 @@ void testPostConfig() { @DisplayName("Update configuration") void testUpdateConfig() { when(repository.findById(UUID.fromString(EDIFACT_EXPORT_CONFIG.getId()))) - .thenReturn(java.util.Optional.of(new ExportConfigEntity())); + .thenReturn(java.util.Optional.of(new ExportConfigEntity().setType(ExportType.EDIFACT_ORDERS_EXPORT.getValue()))); + when(repository.save(any())).thenAnswer(i -> i.getArguments()[0]); service.updateConfig(EDIFACT_EXPORT_CONFIG.getId(), EDIFACT_EXPORT_CONFIG); diff --git a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java index 6193cb35..a33e6caa 100644 --- a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java @@ -58,7 +58,8 @@ void setUp() { setInternalState(defaultExportConfigMapper, "objectMapper", new JacksonConfiguration().entityObjectMapper()); repository = Mockito.mock(ExportConfigRepository.class); - service = new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver); + service = new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, + Mockito.mock(org.folio.des.service.config.ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper()); } @Test @@ -98,7 +99,8 @@ void testUpdateConfig() { var exportConfig = getBursarExportConfig(); when(repository.findById(UUID.fromString(exportConfig.getId()))) - .thenReturn(java.util.Optional.of(new ExportConfigEntity())); + .thenReturn(java.util.Optional.of(new ExportConfigEntity().setType(ExportType.INVOICE_EXPORT.getValue()))); + when(repository.save(any())).thenAnswer(i -> i.getArguments()[0]); service.updateConfig(exportConfig.getId(), exportConfig); diff --git a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java index 4ac3532d..fd62df58 100644 --- a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java @@ -19,6 +19,7 @@ import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.repository.ExportConfigRepository; import org.folio.des.scheduling.bursar.BursarExportScheduler; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.validator.BursarFeesFinesExportParametersValidator; import org.folio.des.validator.ExportConfigValidatorResolver; import org.junit.jupiter.api.BeforeEach; @@ -46,7 +47,8 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); bursarExportScheduler = Mockito.mock(BursarExportScheduler.class); - service = new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, bursarExportScheduler); + service = new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, + Mockito.mock(ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper(), bursarExportScheduler); } @Test @@ -68,7 +70,8 @@ void testUpdateConfig() { var exportConfig = getBursarExportConfig(); when(repository.findById(UUID.fromString(exportConfig.getId()))) - .thenReturn(java.util.Optional.of(new ExportConfigEntity())); + .thenReturn(java.util.Optional.of(new ExportConfigEntity().setType(ExportType.BURSAR_FEES_FINES.getValue()))); + when(repository.save(any())).thenAnswer(i -> i.getArguments()[0]); service.updateConfig(exportConfig.getId(), exportConfig); diff --git a/src/test/java/org/folio/des/support/TestKafkaConsumer.java b/src/test/java/org/folio/des/support/TestKafkaConsumer.java new file mode 100644 index 00000000..9addfe9a --- /dev/null +++ b/src/test/java/org/folio/des/support/TestKafkaConsumer.java @@ -0,0 +1,128 @@ +package org.folio.des.support; + +import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG; +import static org.awaitility.Awaitility.await; + +import java.io.Closeable; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.errors.TopicExistsException; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.boot.kafka.autoconfigure.KafkaProperties; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.listener.ContainerProperties; +import org.springframework.kafka.listener.KafkaMessageListenerContainer; +import org.springframework.kafka.listener.MessageListener; + +/** + * Reusable test consumer that subscribes to a single Kafka topic with a raw {@code String} value deserializer and + * buffers every received record. Integration tests use it to assert on the JSON payload of published domain events. + * + *

Adapted from the mod-notes {@code TestKafkaConsumer} pattern. Callers + * {@link #subscribe(String, KafkaProperties)} it, {@link #poll(String)} the buffered records filtered by key, and + * {@link #close()} it when done (it is {@link Closeable}, so it works with try-with-resources).

+ */ +public final class TestKafkaConsumer implements Closeable { + + private static final Duration DEFAULT_POLL_TIMEOUT = Duration.ofMinutes(1); + private static final Duration POLL_INTERVAL = Duration.ofSeconds(1); + + private final KafkaMessageListenerContainer container; + private final BlockingQueue> records = new LinkedBlockingQueue<>(); + private final List> buffer = new ArrayList<>(); + + private TestKafkaConsumer(KafkaMessageListenerContainer container) { + this.container = container; + } + + /** + * Creates and starts a consumer subscribed to the given topic. + * + * @param topic the topic to consume from (already env/tenant qualified) + * @param properties Spring Kafka properties (bootstrap servers point at the embedded broker) + * @return a started consumer; close it when done + */ + public static TestKafkaConsumer subscribe(String topic, KafkaProperties properties) { + createTopic(topic, properties); + properties.getConsumer().setGroupId("mod-data-export-spring-test-group-" + UUID.randomUUID()); + properties.getConsumer().setAutoOffsetReset("earliest"); + Map config = new HashMap<>(properties.buildConsumerProperties()); + config.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + config.put(VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + + var consumerFactory = new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(), new StringDeserializer()); + var containerProperties = new ContainerProperties(topic); + var container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties); + + var consumer = new TestKafkaConsumer(container); + container.setupMessageListener((MessageListener) consumer.records::add); + container.start(); + return consumer; + } + + /** + * Eagerly creates the topic via an admin client so the producer does not race the broker's lazy auto-creation + * (which otherwise surfaces as {@code Topic ... not present in metadata} on the first send). + * + * @param topic the topic to create (no-op if it already exists) + * @param properties Spring Kafka properties (used for the bootstrap servers) + */ + private static void createTopic(String topic, KafkaProperties properties) { + try (var admin = Admin.create(properties.buildAdminProperties())) { + admin.createTopics(List.of(new NewTopic(topic, 1, (short) 1))).all().get(); + } catch (ExecutionException e) { + if (!(e.getCause() instanceof TopicExistsException)) { + throw new IllegalStateException("Failed to create test topic " + topic, e); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while creating test topic " + topic, e); + } + } + + /** + * Waits (up to one minute) until at least one record with the given key has been received and returns all such + * records, failing the calling test if none arrive. + * + * @param key the record key to filter on (the config id) + * @return the matching records + */ + public List> poll(String key) { + var matched = new ArrayList>(); + await().pollInterval(POLL_INTERVAL).atMost(DEFAULT_POLL_TIMEOUT) + .untilAsserted(() -> { + records.drainTo(buffer); + var found = buffer.stream() + .filter(e -> Objects.equals(e.key(), key)) + .toList(); + if (found.isEmpty()) { + throw new AssertionError("No record received yet for key " + key); + } + matched.clear(); + matched.addAll(found); + }); + return matched; + } + + @Override + public void close() { + container.stop(); + } +} + + + + From aaf57bfdb7cf13302fb473016dd795f7a5ef7c93 Mon Sep 17 00:00:00 2001 From: Yevhenii_Kashtalian Date: Thu, 6 Aug 2026 00:55:33 +0300 Subject: [PATCH 2/6] [MODEXPS-315] Refactor Export Configuration services to remove credential handling and implement event redaction --- NEWS.md | 2 +- descriptors/ModuleDescriptor-template.json | 5 ++ .../des/config/ServiceConfiguration.java | 19 ++--- .../des/config/kafka/KafkaConfiguration.java | 2 - .../folio/des/config/kafka/KafkaService.java | 21 ++--- .../des/domain/dto/event/DomainEvent.java | 36 +++++++++ .../des/domain/dto/event/EdiFtpEventDto.java | 25 ++++++ .../dto/event/ExportConfigEventDto.java | 28 +++++++ .../ExportTypeSpecificParametersEventDto.java | 26 +++++++ .../VendorEdiOrdersExportConfigEventDto.java | 35 +++++++++ .../mapper/event/ExportConfigEventMapper.java | 29 +++++++ .../ExportConfigDomainEventService.java | 48 +++++------- .../config/ExportConfigEventProducer.java | 19 ++--- .../acquisition/ClaimsExportService.java | 6 +- .../EdifactOrdersExportService.java | 6 +- .../config/impl/BaseExportConfigService.java | 34 ++------ .../BursarFeesFinesExportConfigService.java | 6 +- .../des/controller/ConfigsControllerTest.java | 78 +++++++++++++++---- .../acquisition/ClaimsExportServiceTest.java | 2 +- .../EdifactOrdersExportServiceTest.java | 3 +- .../impl/BaseExportConfigServiceTest.java | 3 +- ...ursarFeesFinesExportConfigServiceTest.java | 2 +- .../folio/des/support/TestKafkaConsumer.java | 37 ++++++++- 23 files changed, 342 insertions(+), 130 deletions(-) create mode 100644 src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java create mode 100644 src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java create mode 100644 src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java create mode 100644 src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java create mode 100644 src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java diff --git a/NEWS.md b/NEWS.md index 51fc86ed..da12ee02 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,7 @@ ## 2026-07-28 v3.7.0 ### Stories -* Publish Kafka domain events (CREATE/UPDATE) for Export Configuration changes on `folio.ALL.data-export-spring.config` with structural credential redaction +* Publish Kafka domain events (CREATE/UPDATE) for Export Configuration changes on `folio.ALL.data-export.config` with structural credential redaction ## 2026-05-19 v3.6.1 diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index a36e45af..45dce7f4 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -555,6 +555,11 @@ "name": "ENV", "value": "folio" }, + { + "name": "KAFKA_PRODUCER_TENANT_COLLECTION", + "value": "ALL", + "description": "Tenant partitioning mode for Kafka topics. 'ALL' (default, matches folio-kafka-wrapper) publishes every tenant's events to a single topic per entity (e.g. 'folio.ALL.data-export.config'); consumers route by the 'tenant' field in the event envelope. Any other value falls back to per-tenant topics." + }, { "name": "JOB_EXPIRATION_PERIOD_DAYS", "value": "7" diff --git a/src/main/java/org/folio/des/config/ServiceConfiguration.java b/src/main/java/org/folio/des/config/ServiceConfiguration.java index 1471eae5..7eca6a6f 100644 --- a/src/main/java/org/folio/des/config/ServiceConfiguration.java +++ b/src/main/java/org/folio/des/config/ServiceConfiguration.java @@ -41,14 +41,11 @@ import org.folio.des.validator.acquisition.ClaimsExportParametersValidator; import org.folio.des.validator.acquisition.EdifactOrdersExportParametersValidator; import org.quartz.Scheduler; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.validation.Validator; -import com.fasterxml.jackson.databind.ObjectMapper; - @Configuration @ComponentScan("org.folio.des") public class ServiceConfiguration { @@ -87,10 +84,9 @@ BursarFeesFinesExportConfigService bursarExportConfigService(ExportConfigReposit ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, ExportConfigDomainEventService exportConfigDomainEventService, - @Qualifier("entityObjectMapper") ObjectMapper objectMapper, BursarExportScheduler bursarExportScheduler) { return new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, - exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper, bursarExportScheduler); + exportConfigValidatorResolver, exportConfigDomainEventService, bursarExportScheduler); } @Bean @@ -99,10 +95,9 @@ EdifactOrdersExportService edifactOrdersExportService(ExportConfigRepository rep ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, ExportConfigDomainEventService exportConfigDomainEventService, - @Qualifier("entityObjectMapper") ObjectMapper objectMapper, ExportJobScheduler exportJobScheduler) { return new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, - exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper, exportJobScheduler); + exportConfigValidatorResolver, exportConfigDomainEventService, exportJobScheduler); } @Bean @@ -110,10 +105,9 @@ ClaimsExportService claimsExportService(ExportConfigRepository repository, ClaimsExportConfigMapper claimsExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, - ExportConfigDomainEventService exportConfigDomainEventService, - @Qualifier("entityObjectMapper") ObjectMapper objectMapper) { + ExportConfigDomainEventService exportConfigDomainEventService) { return new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, - exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); + exportConfigValidatorResolver, exportConfigDomainEventService); } @Bean @@ -121,10 +115,9 @@ BaseExportConfigService defaultExportConfigService(ExportConfigRepository reposi DefaultExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, - ExportConfigDomainEventService exportConfigDomainEventService, - @Qualifier("entityObjectMapper") ObjectMapper objectMapper) { + ExportConfigDomainEventService exportConfigDomainEventService) { return new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, - exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); + exportConfigValidatorResolver, exportConfigDomainEventService); } @Bean diff --git a/src/main/java/org/folio/des/config/kafka/KafkaConfiguration.java b/src/main/java/org/folio/des/config/kafka/KafkaConfiguration.java index 5da71871..f380e3de 100644 --- a/src/main/java/org/folio/des/config/kafka/KafkaConfiguration.java +++ b/src/main/java/org/folio/des/config/kafka/KafkaConfiguration.java @@ -21,9 +21,7 @@ import org.springframework.kafka.core.ProducerFactory; import org.springframework.kafka.support.serializer.JsonDeserializer; import org.springframework.kafka.support.serializer.JsonSerializer; -import org.springframework.stereotype.Component; -@Component @Configuration @RequiredArgsConstructor public class KafkaConfiguration { diff --git a/src/main/java/org/folio/des/config/kafka/KafkaService.java b/src/main/java/org/folio/des/config/kafka/KafkaService.java index 277d501a..cdb38152 100644 --- a/src/main/java/org/folio/des/config/kafka/KafkaService.java +++ b/src/main/java/org/folio/des/config/kafka/KafkaService.java @@ -42,7 +42,7 @@ public class KafkaService { @Getter public enum Topic { JOB_COMMAND("data-export.job.command"), - CONFIG("data-export-spring.config"); + CONFIG("data-export.config"); private final String topicName; } @@ -85,24 +85,13 @@ private NewTopic toKafkaTopic(String tenant, Topic topic) { } /** - * Returns topic name in the format - `{env}.{tenant}.topicName` + * Returns the tenant-scoped topic name in the format {@code {env}.{tenant}.topicName}. * - * @param topicName initial topic name as {@link String} - * @param tenantId tenant id as {@link String} - * @return topic name as {@link String} object - */ - /** - * Returns the tenant-scoped topic name for the given {@link Topic} in the format {@code {env}.{tenant}.topicName}. - * - * @param topic the logical topic - * @param tenant the tenant id + * @param topicName the logical topic name + * @param tenantId the tenant id * @return the fully-qualified tenant topic name */ - public String getTenantTopicName(Topic topic, String tenant) { - return getTenantTopicName(topic.getTopicName(), tenant); - } - - private String getTenantTopicName(String topicName, String tenantId) { + public String getTenantTopicName(String topicName, String tenantId) { return KafkaUtils.getTenantTopicName(topicName, environment, tenantId); } diff --git a/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java b/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java index e3914ac6..48487fed 100644 --- a/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java +++ b/src/main/java/org/folio/des/domain/dto/event/DomainEvent.java @@ -27,5 +27,41 @@ public class DomainEvent { @JsonProperty("new") private T newValue; + + /** + * Builds a {@code CREATE} event carrying only the post-change snapshot. + * + * @param newValue the newly-created snapshot + * @param tenant the tenant the change happened in + * @return a populated {@code CREATE} domain event + */ + public static DomainEvent createEvent(T newValue, String tenant) { + return DomainEvent.builder() + .eventId(UUID.randomUUID()) + .eventTs(System.currentTimeMillis()) + .tenant(tenant) + .type(DomainEventType.CREATE) + .newValue(newValue) + .build(); + } + + /** + * Builds an {@code UPDATE} event carrying both the pre- and post-change snapshots. + * + * @param oldValue the pre-change snapshot + * @param newValue the post-change snapshot + * @param tenant the tenant the change happened in + * @return a populated {@code UPDATE} domain event + */ + public static DomainEvent updateEvent(T oldValue, T newValue, String tenant) { + return DomainEvent.builder() + .eventId(UUID.randomUUID()) + .eventTs(System.currentTimeMillis()) + .tenant(tenant) + .type(DomainEventType.UPDATE) + .oldValue(oldValue) + .newValue(newValue) + .build(); + } } diff --git a/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java b/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java new file mode 100644 index 00000000..9ddd8dab --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java @@ -0,0 +1,25 @@ +package org.folio.des.domain.dto.event; + +import org.folio.des.domain.dto.EdiFtp; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event-only view of {@link EdiFtp} that structurally omits the FTP {@code password} (and any other + * credential-shaped field). Redaction is enforced by the type system: this record simply has no component to + * carry a secret, so a credential can never reach the event bus regardless of what the source entity holds. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record EdiFtpEventDto( + @JsonProperty("ftpConnMode") EdiFtp.FtpConnModeEnum ftpConnMode, + @JsonProperty("ftpFormat") EdiFtp.FtpFormatEnum ftpFormat, + @JsonProperty("ftpMode") EdiFtp.FtpModeEnum ftpMode, + @JsonProperty("ftpPort") Integer ftpPort, + @JsonProperty("invoiceDirectory") String invoiceDirectory, + @JsonProperty("isPrimaryTransmissionMethod") Boolean isPrimaryTransmissionMethod, + @JsonProperty("notes") String notes, + @JsonProperty("orderDirectory") String orderDirectory, + @JsonProperty("serverAddress") String serverAddress, + @JsonProperty("username") String username) { +} \ No newline at end of file diff --git a/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java b/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java new file mode 100644 index 00000000..2512dd57 --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java @@ -0,0 +1,28 @@ +package org.folio.des.domain.dto.event; + +import java.util.List; + +import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.ExportType; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event-only snapshot of an {@link ExportConfig}. Mirrors the REST DTO field-for-field with the sole exception + * of the {@code exportTypeSpecificParameters} branch, which is retyped to its redacted event variant so no + * credential-shaped field can ever be serialized onto the event bus. Enum and week-day types are reused from + * the REST DTO to guarantee the payload is byte-identical to the REST representation. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ExportConfigEventDto( + @JsonProperty("id") String id, + @JsonProperty("type") ExportType type, + @JsonProperty("configName") String configName, + @JsonProperty("tenant") String tenant, + @JsonProperty("exportTypeSpecificParameters") ExportTypeSpecificParametersEventDto exportTypeSpecificParameters, + @JsonProperty("scheduleFrequency") Integer scheduleFrequency, + @JsonProperty("schedulePeriod") ExportConfig.SchedulePeriodEnum schedulePeriod, + @JsonProperty("scheduleTime") String scheduleTime, + @JsonProperty("weekDays") List weekDays) { +} \ No newline at end of file diff --git a/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java b/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java new file mode 100644 index 00000000..48bfc0d3 --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java @@ -0,0 +1,26 @@ +package org.folio.des.domain.dto.event; + +import org.folio.des.domain.dto.AuthorityControlExportConfig; +import org.folio.des.domain.dto.BursarExportJob; +import org.folio.des.domain.dto.EHoldingsExportConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event-only view of {@code ExportTypeSpecificParameters}. Only the credential-bearing branch + * ({@code vendorEdiOrdersExportConfig}) is retyped to its redacted event variant; the remaining branches carry + * no secrets and reuse the generated DTOs directly. + * + *

The {@code eHoldingsExportConfig} component keeps the lower-camel name of the generated REST DTO so both + * MapStruct (target = record component name) and the source accessor ({@code geteHoldingsExportConfig}) resolve + * to the same property.

+ */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ExportTypeSpecificParametersEventDto( + @JsonProperty("bursarFeeFines") BursarExportJob bursarFeeFines, + @JsonProperty("vendorEdiOrdersExportConfig") VendorEdiOrdersExportConfigEventDto vendorEdiOrdersExportConfig, + @JsonProperty("query") String query, + @JsonProperty("eHoldingsExportConfig") EHoldingsExportConfig eHoldingsExportConfig, + @JsonProperty("authorityControlExportConfig") AuthorityControlExportConfig authorityControlExportConfig) { +} \ No newline at end of file diff --git a/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java b/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java new file mode 100644 index 00000000..bf6206bf --- /dev/null +++ b/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java @@ -0,0 +1,35 @@ +package org.folio.des.domain.dto.event; + +import java.util.List; +import java.util.UUID; + +import org.folio.des.domain.dto.EdiConfig; +import org.folio.des.domain.dto.EdiEmail; +import org.folio.des.domain.dto.EdiSchedule; +import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Event-only view of {@link VendorEdiOrdersExportConfig}. Identical to the REST DTO except that {@code ediFtp} + * is typed as the redacted {@link EdiFtpEventDto} so no FTP credential can be carried on the event payload. + * Non credential-bearing nested types ({@link EdiConfig}, {@link EdiEmail}, {@link EdiSchedule}) are reused + * directly — none of them declare a credential-shaped field. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record VendorEdiOrdersExportConfigEventDto( + @JsonProperty("exportConfigId") UUID exportConfigId, + @JsonProperty("vendorId") UUID vendorId, + @JsonProperty("configName") String configName, + @JsonProperty("configDescription") String configDescription, + @JsonProperty("ediConfig") EdiConfig ediConfig, + @JsonProperty("ediEmail") EdiEmail ediEmail, + @JsonProperty("ediFtp") EdiFtpEventDto ediFtp, + @JsonProperty("ediSchedule") EdiSchedule ediSchedule, + @JsonProperty("isDefaultConfig") Boolean isDefaultConfig, + @JsonProperty("integrationType") VendorEdiOrdersExportConfig.IntegrationTypeEnum integrationType, + @JsonProperty("transmissionMethod") VendorEdiOrdersExportConfig.TransmissionMethodEnum transmissionMethod, + @JsonProperty("fileFormat") VendorEdiOrdersExportConfig.FileFormatEnum fileFormat, + @JsonProperty("claimPieceIds") List claimPieceIds) { +} diff --git a/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java b/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java new file mode 100644 index 00000000..390e77b8 --- /dev/null +++ b/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java @@ -0,0 +1,29 @@ +package org.folio.des.mapper.event; + +import org.folio.des.domain.dto.EdiFtp; +import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.ExportTypeSpecificParameters; +import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; +import org.folio.des.domain.dto.event.EdiFtpEventDto; +import org.folio.des.domain.dto.event.ExportConfigEventDto; +import org.folio.des.domain.dto.event.ExportTypeSpecificParametersEventDto; +import org.folio.des.domain.dto.event.VendorEdiOrdersExportConfigEventDto; +import org.mapstruct.Mapper; + +/** + * Maps the REST {@link ExportConfig} onto the redacted {@link ExportConfigEventDto} published on the event bus. + * + *

Redaction is structural: the {@link EdiFtpEventDto} target simply has no {@code password} field, so the + * source {@code EdiFtp.password} is left unmapped and can never reach the payload. + */ +@Mapper(componentModel = "spring") +public interface ExportConfigEventMapper { + + ExportConfigEventDto toEventDto(ExportConfig exportConfig); + + ExportTypeSpecificParametersEventDto toEventDto(ExportTypeSpecificParameters parameters); + + VendorEdiOrdersExportConfigEventDto toEventDto(VendorEdiOrdersExportConfig config); + + EdiFtpEventDto toEventDto(EdiFtp ediFtp); +} diff --git a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java index 5b8c8501..5cdb807e 100644 --- a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java +++ b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java @@ -4,7 +4,8 @@ import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.event.DomainEvent; -import org.folio.des.domain.dto.event.DomainEventType; +import org.folio.des.domain.dto.event.ExportConfigEventDto; +import org.folio.des.mapper.event.ExportConfigEventMapper; import org.folio.spring.FolioExecutionContext; import org.springframework.stereotype.Service; @@ -14,6 +15,10 @@ /** * Builds {@link DomainEvent} envelopes for Export Configuration changes and hands them to the * {@link ExportConfigEventProducer}. Config services delegate here so they never build events directly. + * + *

The full REST {@link ExportConfig} snapshot is mapped through {@link ExportConfigEventMapper} to the + * credential-free {@link ExportConfigEventDto} before it leaves this service — redaction happens exactly once, + * here, and is enforced by the target type rather than by a runtime stripping step.

*/ @Log4j2 @Service @@ -21,51 +26,34 @@ public class ExportConfigDomainEventService { private final ExportConfigEventProducer exportConfigEventProducer; + private final ExportConfigEventMapper exportConfigEventMapper; private final FolioExecutionContext folioExecutionContext; /** * Publishes a {@code CREATE} Export Configuration event carrying the new snapshot. * - * @param newConfig the newly-created, sanitized configuration snapshot + * @param newConfig the newly-created configuration snapshot */ public void publishConfigCreatedEvent(ExportConfig newConfig) { - publish(newConfig.getId(), createEvent(newConfig)); + var event = DomainEvent.createEvent(exportConfigEventMapper.toEventDto(newConfig), folioExecutionContext.getTenantId()); + publish(newConfig.getId(), event); } /** * Publishes an {@code UPDATE} Export Configuration event carrying both the pre- and post-change snapshots. * - * @param oldConfig the pre-change, sanitized configuration snapshot - * @param newConfig the post-change, sanitized configuration snapshot + * @param oldConfig the pre-change configuration snapshot + * @param newConfig the post-change configuration snapshot */ public void publishConfigUpdatedEvent(ExportConfig oldConfig, ExportConfig newConfig) { - publish(newConfig.getId(), updateEvent(oldConfig, newConfig)); + var event = DomainEvent.updateEvent(exportConfigEventMapper.toEventDto(oldConfig), + exportConfigEventMapper.toEventDto(newConfig), folioExecutionContext.getTenantId()); + publish(newConfig.getId(), event); } - private DomainEvent createEvent(ExportConfig newConfig) { - return DomainEvent.builder() - .eventId(UUID.randomUUID()) - .eventTs(System.currentTimeMillis()) - .tenant(folioExecutionContext.getTenantId()) - .type(DomainEventType.CREATE) - .newValue(newConfig) - .build(); - } - - private DomainEvent updateEvent(ExportConfig oldConfig, ExportConfig newConfig) { - return DomainEvent.builder() - .eventId(UUID.randomUUID()) - .eventTs(System.currentTimeMillis()) - .tenant(folioExecutionContext.getTenantId()) - .type(DomainEventType.UPDATE) - .oldValue(oldConfig) - .newValue(newConfig) - .build(); - } - - private void publish(String configId, DomainEvent event) { + private void publish(String configId, DomainEvent event) { log.debug("publish:: publishing config event [id: {}, type: {}, tenant: {}]", configId, event.getType(), event.getTenant()); - exportConfigEventProducer.publish(configId, event); + exportConfigEventProducer.publish(UUID.fromString(configId), event); } -} +} \ No newline at end of file diff --git a/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java index 574c38ba..4d045800 100644 --- a/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java +++ b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java @@ -1,10 +1,11 @@ package org.folio.des.service.config; +import java.util.UUID; + import org.folio.des.config.kafka.KafkaService; import org.folio.des.config.kafka.KafkaService.Topic; -import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.event.DomainEvent; -import org.folio.spring.FolioExecutionContext; +import org.folio.des.domain.dto.event.ExportConfigEventDto; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; @@ -25,23 +26,23 @@ public class ExportConfigEventProducer { private final KafkaTemplate kafkaTemplate; private final KafkaService kafkaService; - private final FolioExecutionContext folioExecutionContext; /** - * Publishes an Export Configuration domain event to the tenant-scoped topic. + * Publishes an Export Configuration domain event to the tenant-scoped topic. The tenant is taken from the + * event envelope so the topic resolution and the {@code tenant} field on the payload can never diverge. * * @param configId the export configuration id, used as the Kafka record key * @param event the domain event envelope to publish */ - public void publish(String configId, DomainEvent event) { - var topic = kafkaService.getTenantTopicName(Topic.CONFIG, folioExecutionContext.getTenantId()); + public void publish(UUID configId, DomainEvent event) { + var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), event.getTenant()); + var key = configId.toString(); try { log.info("publish:: Publishing {} event for config id={} on topic={}", event.getType(), configId, topic); - kafkaTemplate.send(topic, configId, event); + kafkaTemplate.send(topic, key, event); log.info("publish:: Successfully published {} event for config id={}", event.getType(), configId); } catch (Exception e) { log.error("publish:: Failed to publish {} event for config id={} on topic={}", event.getType(), configId, topic, e); } } -} - +} \ No newline at end of file diff --git a/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java b/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java index 67f8904a..a3246304 100644 --- a/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java +++ b/src/main/java/org/folio/des/service/config/acquisition/ClaimsExportService.java @@ -11,8 +11,6 @@ import org.folio.des.service.config.impl.BaseExportConfigService; import org.folio.des.validator.ExportConfigValidatorResolver; -import com.fasterxml.jackson.databind.ObjectMapper; - import java.util.Optional; import java.util.UUID; @@ -21,8 +19,8 @@ public class ClaimsExportService extends BaseExportConfigService { public ClaimsExportService(ExportConfigRepository repository, BaseExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, - ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); + ExportConfigDomainEventService exportConfigDomainEventService) { + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService); } @Override diff --git a/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java b/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java index 6b6e46e5..efcbda6b 100644 --- a/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java +++ b/src/main/java/org/folio/des/service/config/acquisition/EdifactOrdersExportService.java @@ -13,8 +13,6 @@ import org.folio.des.service.config.impl.BaseExportConfigService; import org.folio.des.validator.ExportConfigValidatorResolver; -import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.extern.log4j.Log4j2; @Log4j2 @@ -24,9 +22,9 @@ public class EdifactOrdersExportService extends BaseExportConfigService { public EdifactOrdersExportService(ExportConfigRepository repository, BaseExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, - ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper, + ExportConfigDomainEventService exportConfigDomainEventService, ExportJobScheduler exportJobScheduler) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService); this.exportJobScheduler = exportJobScheduler; } diff --git a/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java b/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java index 4b080a3b..3da93c29 100644 --- a/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java +++ b/src/main/java/org/folio/des/service/config/impl/BaseExportConfigService.java @@ -7,7 +7,6 @@ import org.folio.de.entity.ExportConfigEntity; import org.folio.des.domain.dto.ExportType; -import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; import org.folio.des.mapper.BaseExportConfigMapper; import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.domain.dto.ExportConfig; @@ -23,8 +22,6 @@ import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; -import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; @@ -38,7 +35,6 @@ public class BaseExportConfigService implements ExportConfigService { protected final ExportConfigMapperResolver exportConfigMapperResolver; protected final ExportConfigValidatorResolver exportConfigValidatorResolver; protected final ExportConfigDomainEventService exportConfigDomainEventService; - protected final ObjectMapper objectMapper; @Override @Transactional @@ -46,13 +42,13 @@ public void updateConfig(String configId, ExportConfig exportConfig) { log.info("updateConfig:: configId={}, exportConfig={}", configId, exportConfig); validateIncomingExportConfig(exportConfig); var existingEntity = getExportConfigEntityOrThrow(configId); - var oldSnapshot = sanitize(toDto(existingEntity)); + var oldSnapshot = toDto(existingEntity); var entity = exportConfigMapper.toEntity(exportConfig); entity = repository.save(entity); log.info("updateConfig:: Successfully updated config with id={}", configId); - var newSnapshot = sanitize(toDto(entity)); + var newSnapshot = toDto(entity); exportConfigDomainEventService.publishConfigUpdatedEvent(oldSnapshot, newSnapshot); } @@ -64,11 +60,12 @@ public ExportConfig postConfig(ExportConfig exportConfig) { var entity = exportConfigMapper.toEntity(exportConfig); entity = repository.save(entity); - log.info("postConfig:: Successfully created config with id={}", exportConfig.getId()); + log.info("postConfig:: Successfully created config with id={}", entity.getId()); - exportConfigDomainEventService.publishConfigCreatedEvent(sanitize(toDto(entity))); + var savedConfig = toDto(entity); + exportConfigDomainEventService.publishConfigCreatedEvent(savedConfig); - return toDto(entity); + return savedConfig; } @Override @@ -111,25 +108,6 @@ protected ExportConfig toDto(ExportConfigEntity exportConfigEntity) { return exportConfigMapperResolver.resolve(ExportType.fromValue(exportConfigEntity.getType())).toDto(exportConfigEntity); } - /** - * Returns a deep copy of the given configuration with all credential-shaped fields removed, safe to publish on the - * event bus. The original object is left untouched so the REST response still carries the full data. - * - * @param config the configuration snapshot to sanitize - * @return a sanitized deep copy, or {@code null} if the input is {@code null} - */ - protected ExportConfig sanitize(ExportConfig config) { - if (config == null) { - return null; - } - var sanitized = objectMapper.convertValue(config, ExportConfig.class); - Optional.ofNullable(sanitized.getExportTypeSpecificParameters()) - .map(ExportTypeSpecificParameters::getVendorEdiOrdersExportConfig) - .map(VendorEdiOrdersExportConfig::getEdiFtp) - .ifPresent(ediFtp -> ediFtp.setPassword(null)); - return sanitized; - } - protected void validateIncomingExportConfig(ExportConfig exportConfig) { exportConfigValidatorResolver.resolve(exportConfig.getType(), ExportTypeSpecificParameters.class).ifPresent(validator -> { Errors errors = new BeanPropertyBindingResult(exportConfig.getExportTypeSpecificParameters(), "specificParameters"); diff --git a/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java b/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java index 8d2a7b83..959b3fc9 100644 --- a/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java +++ b/src/main/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigService.java @@ -18,8 +18,6 @@ import org.folio.des.validator.ExportConfigValidatorResolver; import org.springframework.data.domain.PageRequest; -import com.fasterxml.jackson.databind.ObjectMapper; - import lombok.extern.log4j.Log4j2; @Log4j2 @@ -29,9 +27,9 @@ public class BursarFeesFinesExportConfigService extends BaseExportConfigService public BursarFeesFinesExportConfigService(ExportConfigRepository repository, DefaultExportConfigMapper defaultExportConfigMapper, ExportConfigMapperResolver exportConfigMapperResolver, ExportConfigValidatorResolver exportConfigValidatorResolver, - ExportConfigDomainEventService exportConfigDomainEventService, ObjectMapper objectMapper, + ExportConfigDomainEventService exportConfigDomainEventService, BursarExportScheduler bursarExportScheduler) { - super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService, objectMapper); + super(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, exportConfigDomainEventService); this.bursarExportScheduler = bursarExportScheduler; } diff --git a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java index 1d7f0be6..2f62f3e0 100644 --- a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java +++ b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java @@ -16,8 +16,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.Duration; import java.util.Objects; +import org.apache.kafka.clients.consumer.ConsumerRecord; import org.folio.des.config.kafka.KafkaService; import org.folio.des.config.kafka.KafkaService.Topic; import org.folio.des.domain.dto.ExportConfig; @@ -48,14 +50,21 @@ class ConfigsControllerTest extends BaseTest { private static final String NEW_CONFIG_REQUEST = "{\"id\":\"0a3cba78-16e7-498e-b75b-98713000277b\",\"type\":\"BURSAR_FEES_FINES\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"filter\":{\"type\":\"Pass\"},\"groupByPatron\":false,\"header\":[],\"data\":[],\"footer\":[],\"transferInfo\":{\"conditions\":[],\"else\":{\"account\":\"90c1820f-60bf-4b9a-99f5-d677ea78ddca\"}}}},\"scheduleFrequency\":5,\"schedulePeriod\":\"DAY\",\"scheduleTime\":\"00:20:00.000Z\"}"; + // schedulePeriod deliberately differs from NEW_CONFIG_REQUEST (DAY -> HOUR) so the UPDATE event can be + // asserted to carry an old snapshot that differs from the new one (AC2). private static final String UPDATE_CONFIG_REQUEST = - "{\"id\":\"0a3cba78-16e7-498e-b75b-98713000277b\",\"type\":\"BURSAR_FEES_FINES\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"filter\":{\"type\":\"Pass\"},\"groupByPatron\":false,\"header\":[],\"data\":[],\"footer\":[],\"transferInfo\":{\"conditions\":[],\"else\":{\"account\":\"90c1820f-60bf-4b9a-99f5-d677ea78ddca\"}}}},\"scheduleFrequency\":5,\"schedulePeriod\":\"DAY\",\"scheduleTime\":\"00:20:00.000Z\"}"; + "{\"id\":\"0a3cba78-16e7-498e-b75b-98713000277b\",\"type\":\"BURSAR_FEES_FINES\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"filter\":{\"type\":\"Pass\"},\"groupByPatron\":false,\"header\":[],\"data\":[],\"footer\":[],\"transferInfo\":{\"conditions\":[],\"else\":{\"account\":\"90c1820f-60bf-4b9a-99f5-d677ea78ddca\"}}}},\"scheduleFrequency\":5,\"schedulePeriod\":\"HOUR\",\"scheduleTime\":\"00:20:00.000Z\"}"; + private static final String FAILED_CONFIG_ID = "c8303ff3-7dec-49a1-acc8-7ce4f311fe21"; private static final String UPDATE_CONFIG_REQUEST_FAILED = - "{\"id\":\"0a3cba78-16e7-498e-b75b-98713000277b\",\"type\":\"BURSAR_FEES_FINES\",\"scheduleFrequency\":5,\"schedulePeriod\":\"DAY\",\"scheduleTime\":\"00:20:00.000Z\"}"; + "{\"id\":\"c8303ff3-7dec-49a1-acc8-7ce4f311fe21\",\"type\":\"BURSAR_FEES_FINES\",\"scheduleFrequency\":5,\"schedulePeriod\":\"DAY\",\"scheduleTime\":\"00:20:00.000Z\"}"; private static final String EDIFACT_CONFIG_REQUEST = "{\"id\":\"5a3cba28-16e7-498e-b73b-98713000298e\", \"type\": \"EDIFACT_ORDERS_EXPORT\", \"exportTypeSpecificParameters\": { \"vendorEdiOrdersExportConfig\": {\"vendorId\": \"046b6c7f-0b8a-43b9-b35d-6489e6daee91\", \"configName\": \"edi_config\", \"integrationType\": \"Ordering\", \"fileFormat\": \"CSV\", \"transmissionMethod\": \"File download\", \"ediSchedule\": {\"enableScheduledExport\": true, \"scheduleParameters\": {\"scheduleFrequency\": 1, \"schedulePeriod\": \"HOUR\", \"scheduleTime\": \"15:30:00\"}}}}, \"schedulePeriod\": \"HOUR\"}"; private static final String CLAIMS_REQUEST = "{\"id\":\"30ad9c6d-f2e7-425f-a171-b4e0cbce7204\",\"type\":\"CLAIMS\",\"tenant\":\"diku\",\"exportTypeSpecificParameters\":{\"vendorEdiOrdersExportConfig\":{\"exportConfigId\":\"30ad9c6d-f2e7-425f-a171-b4e0cbce7204\",\"vendorId\":\"1e958895-82a6-4fa1-b6fe-763063381946\",\"configName\":\"Test 1-3\",\"ediConfig\":{\"accountNoList\":[\"3\"],\"ediNamingConvention\":\"{organizationCode}-{integrationName}-{exportJobEndDate}\",\"libEdiType\":\"31B/US-SAN\",\"vendorEdiType\":\"31B/US-SAN\",\"sendAccountNumber\":false,\"supportOrder\":false,\"supportInvoice\":false},\"ediFtp\":{\"ftpConnMode\":\"Active\",\"ftpFormat\":\"SFTP\",\"ftpMode\":\"ASCII\"},\"isDefaultConfig\":false,\"integrationType\":\"Claiming\",\"transmissionMethod\":\"File download\",\"fileFormat\":\"CSV\"}},\"schedulePeriod\":\"NONE\"}"; + private static final String PASSWORD_SENTINEL = "AC3-sentinel-12345"; + private static final String CLAIMS_WITH_PASSWORD_ID = "12345678-1234-1234-1234-1234567890ab"; + private static final String CLAIMS_REQUEST_WITH_PASSWORD = + "{\"id\":\"12345678-1234-1234-1234-1234567890ab\",\"type\":\"CLAIMS\",\"tenant\":\"diku\",\"exportTypeSpecificParameters\":{\"vendorEdiOrdersExportConfig\":{\"exportConfigId\":\"12345678-1234-1234-1234-1234567890ab\",\"vendorId\":\"1e958895-82a6-4fa1-b6fe-763063381946\",\"configName\":\"Test 1-3\",\"ediConfig\":{\"accountNoList\":[\"3\"],\"ediNamingConvention\":\"{organizationCode}-{integrationName}-{exportJobEndDate}\",\"libEdiType\":\"31B/US-SAN\",\"vendorEdiType\":\"31B/US-SAN\",\"sendAccountNumber\":false,\"supportOrder\":false,\"supportInvoice\":false},\"ediFtp\":{\"ftpConnMode\":\"Active\",\"ftpFormat\":\"SFTP\",\"ftpMode\":\"ASCII\",\"username\":\"ftp-user\",\"password\":\"AC3-sentinel-12345\"},\"isDefaultConfig\":false,\"integrationType\":\"Claiming\",\"transmissionMethod\":\"File download\",\"fileFormat\":\"CSV\"}},\"schedulePeriod\":\"NONE\"}"; @Autowired private MockMvc mockMvc; @@ -196,6 +205,29 @@ void postClaimsConfig() throws Exception { content().contentType("text/plain;charset=UTF-8")); } + @Test + @DisplayName("Should redact all credential-shaped fields from the published config event (AC3)") + void postConfigShouldRedactCredentials() throws Exception { + mockMvc + .perform( + post("/data-export-spring/configs") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .headers(defaultHeaders()) + .content(CLAIMS_REQUEST_WITH_PASSWORD)) + .andExpectAll(status().isCreated()); + + var eventJson = pollConfigEventRawJson(CLAIMS_WITH_PASSWORD_ID, DomainEventType.CREATE); + + assertThat(eventJson) + .doesNotContain(PASSWORD_SENTINEL) + .doesNotContain("\"password\"") + .doesNotContain("\"secret\"") + .doesNotContain("\"token\"") + .doesNotContain("\"apiKey\"") + .doesNotContain("\"credential\"") + .doesNotContain("\"privateKey\""); + } + @Test @DisplayName("Success update config") void putConfig() throws Exception { @@ -222,6 +254,10 @@ void putConfig() throws Exception { .usingRecursiveComparison() .ignoringFields("configName", "tenant") .isEqualTo(OBJECT_MAPPER.readValue(UPDATE_CONFIG_REQUEST, ExportConfig.class)); + + assertThat(event.getOldValue().getSchedulePeriod()).isEqualTo(ExportConfig.SchedulePeriodEnum.DAY); + assertThat(event.getNewValue().getSchedulePeriod()).isEqualTo(ExportConfig.SchedulePeriodEnum.HOUR); + assertThat(event.getOldValue().getId()).isEqualTo(event.getNewValue().getId()); } @Test @@ -240,17 +276,22 @@ void putShouldThrowExceptionConfig() throws Exception { } @Test - @DisplayName("Fail update config") + @DisplayName("Fail update config and publish no event on validation failure (AC4)") void putConfigFail() throws Exception { - mockMvc - .perform( - put("/data-export-spring/configs/c8303ff3-7dec-49a1-acc8-7ce4f311fe21") - .contentType(MediaType.APPLICATION_JSON_VALUE) - .headers(defaultHeaders()) - .content(UPDATE_CONFIG_REQUEST_FAILED)) - .andExpectAll(status().isBadRequest(), - content().contentType(MediaType.APPLICATION_JSON_VALUE), - jsonPath("$.errors[0].message", startsWith("MethodArgumentNotValidException"))); + var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), TENANT); + try (var consumer = TestKafkaConsumer.subscribe(topic, kafkaProperties)) { + mockMvc + .perform( + put("/data-export-spring/configs/" + FAILED_CONFIG_ID) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .headers(defaultHeaders()) + .content(UPDATE_CONFIG_REQUEST_FAILED)) + .andExpectAll(status().isBadRequest(), + content().contentType(MediaType.APPLICATION_JSON_VALUE), + jsonPath("$.errors[0].message", startsWith("MethodArgumentNotValidException"))); + + assertThat(consumer.drainFor(FAILED_CONFIG_ID, Duration.ofSeconds(5))).isEmpty(); + } } @Test @@ -309,7 +350,7 @@ void shouldNotBeDeletedIfConfigIsNotExist() throws Exception { } private DomainEvent pollConfigEventJson(String configId, DomainEventType type) { - var topic = kafkaService.getTenantTopicName(Topic.CONFIG, TENANT); + var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), TENANT); try (var consumer = TestKafkaConsumer.subscribe(topic, kafkaProperties)) { return consumer.poll(configId).stream() .map(event -> readEvent(event.value())) @@ -319,6 +360,17 @@ private DomainEvent pollConfigEventJson(String configId, DomainEve } } + private String pollConfigEventRawJson(String configId, DomainEventType type) { + var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), TENANT); + try (var consumer = TestKafkaConsumer.subscribe(topic, kafkaProperties)) { + return consumer.poll(configId).stream() + .filter(record -> readEvent(record.value()).getType() == type) + .map(ConsumerRecord::value) + .findFirst() + .orElseThrow(() -> new AssertionError("Expected " + type + " event for config " + configId)); + } + } + @SneakyThrows private DomainEvent readEvent(String json) { return OBJECT_MAPPER.readValue(json, new TypeReference<>() {}); diff --git a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java index 1d4de751..0bed4935 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java @@ -63,7 +63,7 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); service = new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper()); + Mockito.mock(ExportConfigDomainEventService.class)); } @Test diff --git a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java index bb755aed..6c7e7115 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java @@ -23,6 +23,7 @@ import org.folio.des.mapper.acquisition.EdifactExportConfigMapperImpl; import org.folio.des.repository.ExportConfigRepository; import org.folio.des.scheduling.ExportJobScheduler; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.validator.ExportConfigValidatorResolver; import org.folio.des.validator.acquisition.EdifactOrdersExportParametersValidator; import org.folio.des.validator.acquisition.EdifactOrdersScheduledParamsValidator; @@ -75,7 +76,7 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); edifactOrdersExportJobScheduler = Mockito.mock(ExportJobScheduler.class); service = new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(org.folio.des.service.config.ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper(), edifactOrdersExportJobScheduler); + Mockito.mock(ExportConfigDomainEventService.class), edifactOrdersExportJobScheduler); } @Test diff --git a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java index a33e6caa..5c2ab0c6 100644 --- a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java @@ -24,6 +24,7 @@ import org.folio.des.mapper.DefaultExportConfigMapper; import org.folio.des.mapper.ExportConfigMapperResolver; import org.folio.des.repository.ExportConfigRepository; +import org.folio.des.service.config.ExportConfigDomainEventService; import org.folio.des.validator.BursarFeesFinesExportParametersValidator; import org.folio.des.validator.ExportConfigValidatorResolver; import org.junit.jupiter.api.Assertions; @@ -59,7 +60,7 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); service = new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(org.folio.des.service.config.ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper()); + Mockito.mock(ExportConfigDomainEventService.class)); } @Test diff --git a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java index fd62df58..7a408d3e 100644 --- a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java @@ -48,7 +48,7 @@ void setUp() { repository = Mockito.mock(ExportConfigRepository.class); bursarExportScheduler = Mockito.mock(BursarExportScheduler.class); service = new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class), new JacksonConfiguration().entityObjectMapper(), bursarExportScheduler); + Mockito.mock(ExportConfigDomainEventService.class), bursarExportScheduler); } @Test diff --git a/src/test/java/org/folio/des/support/TestKafkaConsumer.java b/src/test/java/org/folio/des/support/TestKafkaConsumer.java index 9addfe9a..43beb0e0 100644 --- a/src/test/java/org/folio/des/support/TestKafkaConsumer.java +++ b/src/test/java/org/folio/des/support/TestKafkaConsumer.java @@ -1,5 +1,7 @@ package org.folio.des.support; +import static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.GROUP_ID_CONFIG; import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG; import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG; import static org.awaitility.Awaitility.await; @@ -15,6 +17,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import org.apache.kafka.clients.admin.Admin; import org.apache.kafka.clients.admin.NewTopic; @@ -57,9 +60,11 @@ private TestKafkaConsumer(KafkaMessageListenerContainer containe */ public static TestKafkaConsumer subscribe(String topic, KafkaProperties properties) { createTopic(topic, properties); - properties.getConsumer().setGroupId("mod-data-export-spring-test-group-" + UUID.randomUUID()); - properties.getConsumer().setAutoOffsetReset("earliest"); + // Override consumer settings on a local copy only — never mutate the shared Spring KafkaProperties bean, + // otherwise concurrently-running tests inherit this consumer's group id / offset reset. Map config = new HashMap<>(properties.buildConsumerProperties()); + config.put(GROUP_ID_CONFIG, "mod-data-export-spring-test-group-" + UUID.randomUUID()); + config.put(AUTO_OFFSET_RESET_CONFIG, "earliest"); config.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); config.put(VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); @@ -117,6 +122,34 @@ public List> poll(String key) { return matched; } + /** + * Drains records for up to the given duration and returns every buffered record matching the key (possibly + * empty). Unlike {@link #poll(String)} this never fails on an empty result, so it can assert that no + * event was published. It blocks on the record queue rather than sleeping a fixed interval. + * + * @param key the record key to filter on (the config id) + * @param duration the maximum time to wait for records to arrive + * @return the matching records seen within the window (empty if none arrived) + */ + public List> drainFor(String key, Duration duration) { + long deadline = System.nanoTime() + duration.toNanos(); + long remaining; + try { + while ((remaining = deadline - System.nanoTime()) > 0) { + var record = records.poll(remaining, TimeUnit.NANOSECONDS); + if (record != null) { + buffer.add(record); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while draining test topic", e); + } + return buffer.stream() + .filter(e -> Objects.equals(e.key(), key)) + .toList(); + } + @Override public void close() { container.stop(); From 3d67aa4ad48835daf7fcf4247b9299948af9dcfd Mon Sep 17 00:00:00 2001 From: Yevhenii_Kashtalian Date: Thu, 6 Aug 2026 00:59:25 +0300 Subject: [PATCH 3/6] [MODEXPS-315] Simplify comment --- .../dto/event/ExportTypeSpecificParametersEventDto.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java b/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java index 48bfc0d3..0b1e5cfd 100644 --- a/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java +++ b/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java @@ -11,10 +11,6 @@ * Event-only view of {@code ExportTypeSpecificParameters}. Only the credential-bearing branch * ({@code vendorEdiOrdersExportConfig}) is retyped to its redacted event variant; the remaining branches carry * no secrets and reuse the generated DTOs directly. - * - *

The {@code eHoldingsExportConfig} component keeps the lower-camel name of the generated REST DTO so both - * MapStruct (target = record component name) and the source accessor ({@code geteHoldingsExportConfig}) resolve - * to the same property.

*/ @JsonInclude(JsonInclude.Include.NON_NULL) public record ExportTypeSpecificParametersEventDto( @@ -23,4 +19,4 @@ public record ExportTypeSpecificParametersEventDto( @JsonProperty("query") String query, @JsonProperty("eHoldingsExportConfig") EHoldingsExportConfig eHoldingsExportConfig, @JsonProperty("authorityControlExportConfig") AuthorityControlExportConfig authorityControlExportConfig) { -} \ No newline at end of file +} From 9c284b19be2fccf3f4bf033d4966375be0da5153 Mon Sep 17 00:00:00 2001 From: Yevhenii_Kashtalian Date: Thu, 6 Aug 2026 19:12:58 +0300 Subject: [PATCH 4/6] [MODEXPS-315] Enhance ExportConfigDomainEventService to sanitize credentials before publishing events and remove dtos --- .../des/domain/dto/event/EdiFtpEventDto.java | 25 ---------- .../dto/event/ExportConfigEventDto.java | 28 ----------- .../ExportTypeSpecificParametersEventDto.java | 22 --------- .../VendorEdiOrdersExportConfigEventDto.java | 35 ------------- .../mapper/event/ExportConfigEventMapper.java | 29 ----------- .../ExportConfigDomainEventService.java | 49 ++++++++++++++----- .../config/ExportConfigEventProducer.java | 4 +- 7 files changed, 40 insertions(+), 152 deletions(-) delete mode 100644 src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java delete mode 100644 src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java delete mode 100644 src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java delete mode 100644 src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java delete mode 100644 src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java diff --git a/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java b/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java deleted file mode 100644 index 9ddd8dab..00000000 --- a/src/main/java/org/folio/des/domain/dto/event/EdiFtpEventDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.folio.des.domain.dto.event; - -import org.folio.des.domain.dto.EdiFtp; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Event-only view of {@link EdiFtp} that structurally omits the FTP {@code password} (and any other - * credential-shaped field). Redaction is enforced by the type system: this record simply has no component to - * carry a secret, so a credential can never reach the event bus regardless of what the source entity holds. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record EdiFtpEventDto( - @JsonProperty("ftpConnMode") EdiFtp.FtpConnModeEnum ftpConnMode, - @JsonProperty("ftpFormat") EdiFtp.FtpFormatEnum ftpFormat, - @JsonProperty("ftpMode") EdiFtp.FtpModeEnum ftpMode, - @JsonProperty("ftpPort") Integer ftpPort, - @JsonProperty("invoiceDirectory") String invoiceDirectory, - @JsonProperty("isPrimaryTransmissionMethod") Boolean isPrimaryTransmissionMethod, - @JsonProperty("notes") String notes, - @JsonProperty("orderDirectory") String orderDirectory, - @JsonProperty("serverAddress") String serverAddress, - @JsonProperty("username") String username) { -} \ No newline at end of file diff --git a/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java b/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java deleted file mode 100644 index 2512dd57..00000000 --- a/src/main/java/org/folio/des/domain/dto/event/ExportConfigEventDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.folio.des.domain.dto.event; - -import java.util.List; - -import org.folio.des.domain.dto.ExportConfig; -import org.folio.des.domain.dto.ExportType; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Event-only snapshot of an {@link ExportConfig}. Mirrors the REST DTO field-for-field with the sole exception - * of the {@code exportTypeSpecificParameters} branch, which is retyped to its redacted event variant so no - * credential-shaped field can ever be serialized onto the event bus. Enum and week-day types are reused from - * the REST DTO to guarantee the payload is byte-identical to the REST representation. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ExportConfigEventDto( - @JsonProperty("id") String id, - @JsonProperty("type") ExportType type, - @JsonProperty("configName") String configName, - @JsonProperty("tenant") String tenant, - @JsonProperty("exportTypeSpecificParameters") ExportTypeSpecificParametersEventDto exportTypeSpecificParameters, - @JsonProperty("scheduleFrequency") Integer scheduleFrequency, - @JsonProperty("schedulePeriod") ExportConfig.SchedulePeriodEnum schedulePeriod, - @JsonProperty("scheduleTime") String scheduleTime, - @JsonProperty("weekDays") List weekDays) { -} \ No newline at end of file diff --git a/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java b/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java deleted file mode 100644 index 0b1e5cfd..00000000 --- a/src/main/java/org/folio/des/domain/dto/event/ExportTypeSpecificParametersEventDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.folio.des.domain.dto.event; - -import org.folio.des.domain.dto.AuthorityControlExportConfig; -import org.folio.des.domain.dto.BursarExportJob; -import org.folio.des.domain.dto.EHoldingsExportConfig; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Event-only view of {@code ExportTypeSpecificParameters}. Only the credential-bearing branch - * ({@code vendorEdiOrdersExportConfig}) is retyped to its redacted event variant; the remaining branches carry - * no secrets and reuse the generated DTOs directly. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ExportTypeSpecificParametersEventDto( - @JsonProperty("bursarFeeFines") BursarExportJob bursarFeeFines, - @JsonProperty("vendorEdiOrdersExportConfig") VendorEdiOrdersExportConfigEventDto vendorEdiOrdersExportConfig, - @JsonProperty("query") String query, - @JsonProperty("eHoldingsExportConfig") EHoldingsExportConfig eHoldingsExportConfig, - @JsonProperty("authorityControlExportConfig") AuthorityControlExportConfig authorityControlExportConfig) { -} diff --git a/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java b/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java deleted file mode 100644 index bf6206bf..00000000 --- a/src/main/java/org/folio/des/domain/dto/event/VendorEdiOrdersExportConfigEventDto.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.folio.des.domain.dto.event; - -import java.util.List; -import java.util.UUID; - -import org.folio.des.domain.dto.EdiConfig; -import org.folio.des.domain.dto.EdiEmail; -import org.folio.des.domain.dto.EdiSchedule; -import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Event-only view of {@link VendorEdiOrdersExportConfig}. Identical to the REST DTO except that {@code ediFtp} - * is typed as the redacted {@link EdiFtpEventDto} so no FTP credential can be carried on the event payload. - * Non credential-bearing nested types ({@link EdiConfig}, {@link EdiEmail}, {@link EdiSchedule}) are reused - * directly — none of them declare a credential-shaped field. - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record VendorEdiOrdersExportConfigEventDto( - @JsonProperty("exportConfigId") UUID exportConfigId, - @JsonProperty("vendorId") UUID vendorId, - @JsonProperty("configName") String configName, - @JsonProperty("configDescription") String configDescription, - @JsonProperty("ediConfig") EdiConfig ediConfig, - @JsonProperty("ediEmail") EdiEmail ediEmail, - @JsonProperty("ediFtp") EdiFtpEventDto ediFtp, - @JsonProperty("ediSchedule") EdiSchedule ediSchedule, - @JsonProperty("isDefaultConfig") Boolean isDefaultConfig, - @JsonProperty("integrationType") VendorEdiOrdersExportConfig.IntegrationTypeEnum integrationType, - @JsonProperty("transmissionMethod") VendorEdiOrdersExportConfig.TransmissionMethodEnum transmissionMethod, - @JsonProperty("fileFormat") VendorEdiOrdersExportConfig.FileFormatEnum fileFormat, - @JsonProperty("claimPieceIds") List claimPieceIds) { -} diff --git a/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java b/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java deleted file mode 100644 index 390e77b8..00000000 --- a/src/main/java/org/folio/des/mapper/event/ExportConfigEventMapper.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.folio.des.mapper.event; - -import org.folio.des.domain.dto.EdiFtp; -import org.folio.des.domain.dto.ExportConfig; -import org.folio.des.domain.dto.ExportTypeSpecificParameters; -import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; -import org.folio.des.domain.dto.event.EdiFtpEventDto; -import org.folio.des.domain.dto.event.ExportConfigEventDto; -import org.folio.des.domain.dto.event.ExportTypeSpecificParametersEventDto; -import org.folio.des.domain.dto.event.VendorEdiOrdersExportConfigEventDto; -import org.mapstruct.Mapper; - -/** - * Maps the REST {@link ExportConfig} onto the redacted {@link ExportConfigEventDto} published on the event bus. - * - *

Redaction is structural: the {@link EdiFtpEventDto} target simply has no {@code password} field, so the - * source {@code EdiFtp.password} is left unmapped and can never reach the payload. - */ -@Mapper(componentModel = "spring") -public interface ExportConfigEventMapper { - - ExportConfigEventDto toEventDto(ExportConfig exportConfig); - - ExportTypeSpecificParametersEventDto toEventDto(ExportTypeSpecificParameters parameters); - - VendorEdiOrdersExportConfigEventDto toEventDto(VendorEdiOrdersExportConfig config); - - EdiFtpEventDto toEventDto(EdiFtp ediFtp); -} diff --git a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java index 5cdb807e..9b8e187e 100644 --- a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java +++ b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java @@ -1,14 +1,19 @@ package org.folio.des.service.config; +import java.util.Optional; import java.util.UUID; import org.folio.des.domain.dto.ExportConfig; +import org.folio.des.domain.dto.ExportTypeSpecificParameters; +import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; import org.folio.des.domain.dto.event.DomainEvent; -import org.folio.des.domain.dto.event.ExportConfigEventDto; -import org.folio.des.mapper.event.ExportConfigEventMapper; import org.folio.spring.FolioExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -16,9 +21,10 @@ * Builds {@link DomainEvent} envelopes for Export Configuration changes and hands them to the * {@link ExportConfigEventProducer}. Config services delegate here so they never build events directly. * - *

The full REST {@link ExportConfig} snapshot is mapped through {@link ExportConfigEventMapper} to the - * credential-free {@link ExportConfigEventDto} before it leaves this service — redaction happens exactly once, - * here, and is enforced by the target type rather than by a runtime stripping step.

+ *

Every snapshot is {@link #sanitize(ExportConfig) sanitized} before it leaves this service — the FTP + * password (and any other credential-shaped field) is stripped from a deep copy so it can never reach the event + * bus. The producer's {@code ObjectMapper} omits empty values, so the stripped fields do not appear at all + * (not even as {@code null}) in the serialized payload.

*/ @Log4j2 @Service @@ -26,16 +32,19 @@ public class ExportConfigDomainEventService { private final ExportConfigEventProducer exportConfigEventProducer; - private final ExportConfigEventMapper exportConfigEventMapper; private final FolioExecutionContext folioExecutionContext; + @Autowired + @Qualifier("entityObjectMapper") + private ObjectMapper objectMapper; + /** * Publishes a {@code CREATE} Export Configuration event carrying the new snapshot. * * @param newConfig the newly-created configuration snapshot */ public void publishConfigCreatedEvent(ExportConfig newConfig) { - var event = DomainEvent.createEvent(exportConfigEventMapper.toEventDto(newConfig), folioExecutionContext.getTenantId()); + var event = DomainEvent.createEvent(sanitize(newConfig), folioExecutionContext.getTenantId()); publish(newConfig.getId(), event); } @@ -46,14 +55,32 @@ public void publishConfigCreatedEvent(ExportConfig newConfig) { * @param newConfig the post-change configuration snapshot */ public void publishConfigUpdatedEvent(ExportConfig oldConfig, ExportConfig newConfig) { - var event = DomainEvent.updateEvent(exportConfigEventMapper.toEventDto(oldConfig), - exportConfigEventMapper.toEventDto(newConfig), folioExecutionContext.getTenantId()); + var event = DomainEvent.updateEvent(sanitize(oldConfig), sanitize(newConfig), folioExecutionContext.getTenantId()); publish(newConfig.getId(), event); } - private void publish(String configId, DomainEvent event) { + private void publish(String configId, DomainEvent event) { log.debug("publish:: publishing config event [id: {}, type: {}, tenant: {}]", configId, event.getType(), event.getTenant()); exportConfigEventProducer.publish(UUID.fromString(configId), event); } -} \ No newline at end of file + + /** + * Returns a deep copy of the given configuration with all credential-shaped fields removed, safe to publish on + * the event bus. The original object is left untouched so the REST response still carries the full data. + * + * @param config the configuration snapshot to sanitize + * @return a sanitized deep copy, or {@code null} if the input is {@code null} + */ + private ExportConfig sanitize(ExportConfig config) { + if (config == null) { + return null; + } + var sanitized = objectMapper.convertValue(config, ExportConfig.class); + Optional.ofNullable(sanitized.getExportTypeSpecificParameters()) + .map(ExportTypeSpecificParameters::getVendorEdiOrdersExportConfig) + .map(VendorEdiOrdersExportConfig::getEdiFtp) + .ifPresent(ediFtp -> ediFtp.setPassword(null)); + return sanitized; + } +} diff --git a/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java index 4d045800..bd23d8cb 100644 --- a/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java +++ b/src/main/java/org/folio/des/service/config/ExportConfigEventProducer.java @@ -4,8 +4,8 @@ import org.folio.des.config.kafka.KafkaService; import org.folio.des.config.kafka.KafkaService.Topic; +import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.event.DomainEvent; -import org.folio.des.domain.dto.event.ExportConfigEventDto; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; @@ -34,7 +34,7 @@ public class ExportConfigEventProducer { * @param configId the export configuration id, used as the Kafka record key * @param event the domain event envelope to publish */ - public void publish(UUID configId, DomainEvent event) { + public void publish(UUID configId, DomainEvent event) { var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), event.getTenant()); var key = configId.toString(); try { From 85c38c7ae3f82fa977f4acbd638d0038b590fdb2 Mon Sep 17 00:00:00 2001 From: Yevhenii_Kashtalian Date: Fri, 7 Aug 2026 09:32:26 +0300 Subject: [PATCH 5/6] [MODEXPS-315] Refactor ExportConfigDomainEventService --- .../ExportConfigDomainEventService.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java index 9b8e187e..5b99fea0 100644 --- a/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java +++ b/src/main/java/org/folio/des/service/config/ExportConfigDomainEventService.java @@ -3,18 +3,17 @@ import java.util.Optional; import java.util.UUID; +import org.apache.commons.lang3.StringUtils; import org.folio.des.domain.dto.ExportConfig; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; import org.folio.des.domain.dto.event.DomainEvent; import org.folio.spring.FolioExecutionContext; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; /** @@ -28,15 +27,19 @@ */ @Log4j2 @Service -@RequiredArgsConstructor public class ExportConfigDomainEventService { private final ExportConfigEventProducer exportConfigEventProducer; private final FolioExecutionContext folioExecutionContext; + private final ObjectMapper objectMapper; - @Autowired - @Qualifier("entityObjectMapper") - private ObjectMapper objectMapper; + public ExportConfigDomainEventService(ExportConfigEventProducer exportConfigEventProducer, + FolioExecutionContext folioExecutionContext, + @Qualifier("entityObjectMapper") ObjectMapper objectMapper) { + this.exportConfigEventProducer = exportConfigEventProducer; + this.folioExecutionContext = folioExecutionContext; + this.objectMapper = objectMapper; + } /** * Publishes a {@code CREATE} Export Configuration event carrying the new snapshot. @@ -44,6 +47,10 @@ public class ExportConfigDomainEventService { * @param newConfig the newly-created configuration snapshot */ public void publishConfigCreatedEvent(ExportConfig newConfig) { + if (newConfig == null || StringUtils.isBlank(newConfig.getId())) { + log.warn("publishConfigCreatedEvent:: skipping CREATE event, config id is missing"); + return; + } var event = DomainEvent.createEvent(sanitize(newConfig), folioExecutionContext.getTenantId()); publish(newConfig.getId(), event); } @@ -55,6 +62,10 @@ public void publishConfigCreatedEvent(ExportConfig newConfig) { * @param newConfig the post-change configuration snapshot */ public void publishConfigUpdatedEvent(ExportConfig oldConfig, ExportConfig newConfig) { + if (newConfig == null || StringUtils.isBlank(newConfig.getId())) { + log.warn("publishConfigUpdatedEvent:: skipping UPDATE event, config id is missing"); + return; + } var event = DomainEvent.updateEvent(sanitize(oldConfig), sanitize(newConfig), folioExecutionContext.getTenantId()); publish(newConfig.getId(), event); } @@ -77,10 +88,11 @@ private ExportConfig sanitize(ExportConfig config) { return null; } var sanitized = objectMapper.convertValue(config, ExportConfig.class); - Optional.ofNullable(sanitized.getExportTypeSpecificParameters()) + Optional.ofNullable(sanitized) + .map(ExportConfig::getExportTypeSpecificParameters) .map(ExportTypeSpecificParameters::getVendorEdiOrdersExportConfig) .map(VendorEdiOrdersExportConfig::getEdiFtp) .ifPresent(ediFtp -> ediFtp.setPassword(null)); return sanitized; } -} +} \ No newline at end of file From 783454e09b8e50e76358eab4bbb05950c019cb94 Mon Sep 17 00:00:00 2001 From: Yevhenii_Kashtalian Date: Fri, 7 Aug 2026 10:34:44 +0300 Subject: [PATCH 6/6] [MODEXPS-315] Fix sonar issues --- .../org/folio/des/controller/ConfigsControllerTest.java | 2 +- .../config/acquisition/ClaimsExportServiceTest.java | 6 +++--- .../acquisition/EdifactOrdersExportServiceTest.java | 8 ++++---- .../service/config/impl/BaseExportConfigServiceTest.java | 6 +++--- .../impl/BursarFeesFinesExportConfigServiceTest.java | 8 ++++---- .../java/org/folio/des/support/TestKafkaConsumer.java | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java index 2f62f3e0..1742dd6c 100644 --- a/src/test/java/org/folio/des/controller/ConfigsControllerTest.java +++ b/src/test/java/org/folio/des/controller/ConfigsControllerTest.java @@ -364,7 +364,7 @@ private String pollConfigEventRawJson(String configId, DomainEventType type) { var topic = kafkaService.getTenantTopicName(Topic.CONFIG.getTopicName(), TENANT); try (var consumer = TestKafkaConsumer.subscribe(topic, kafkaProperties)) { return consumer.poll(configId).stream() - .filter(record -> readEvent(record.value()).getType() == type) + .filter(event -> readEvent(event.value()).getType() == type) .map(ConsumerRecord::value) .findFirst() .orElseThrow(() -> new AssertionError("Expected " + type + " event for config " + configId)); diff --git a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java index 0bed4935..087351cf 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/ClaimsExportServiceTest.java @@ -3,6 +3,7 @@ import static org.folio.des.support.TestUtils.setInternalState; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,7 +27,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -61,9 +61,9 @@ void setUp() { setInternalState(claimsExportConfigMapper, "objectMapper", new JacksonConfiguration().entityObjectMapper()); setInternalState(claimsExportConfigMapper, "validator", validator); - repository = Mockito.mock(ExportConfigRepository.class); + repository = mock(ExportConfigRepository.class); service = new ClaimsExportService(repository, claimsExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class)); + mock(ExportConfigDomainEventService.class)); } @Test diff --git a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java index 6c7e7115..f15d9fa0 100644 --- a/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java +++ b/src/test/java/org/folio/des/service/config/acquisition/EdifactOrdersExportServiceTest.java @@ -3,6 +3,7 @@ import static org.folio.des.support.TestUtils.setInternalState; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -31,7 +32,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -73,10 +73,10 @@ void setUp() { setInternalState(edifactExportConfigMapper, "objectMapper", new JacksonConfiguration().entityObjectMapper()); setInternalState(edifactExportConfigMapper, "validator", validator); - repository = Mockito.mock(ExportConfigRepository.class); - edifactOrdersExportJobScheduler = Mockito.mock(ExportJobScheduler.class); + repository = mock(ExportConfigRepository.class); + edifactOrdersExportJobScheduler = mock(ExportJobScheduler.class); service = new EdifactOrdersExportService(repository, edifactExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class), edifactOrdersExportJobScheduler); + mock(ExportConfigDomainEventService.class), edifactOrdersExportJobScheduler); } @Test diff --git a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java index 5c2ab0c6..00ead768 100644 --- a/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BaseExportConfigServiceTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,7 +33,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -58,9 +58,9 @@ void setUp() { var exportConfigMapperResolver = new ExportConfigMapperResolver(Map.of(), defaultExportConfigMapper); setInternalState(defaultExportConfigMapper, "objectMapper", new JacksonConfiguration().entityObjectMapper()); - repository = Mockito.mock(ExportConfigRepository.class); + repository = mock(ExportConfigRepository.class); service = new BaseExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class)); + mock(ExportConfigDomainEventService.class)); } @Test diff --git a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java index 7a408d3e..435a241f 100644 --- a/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java +++ b/src/test/java/org/folio/des/service/config/impl/BursarFeesFinesExportConfigServiceTest.java @@ -4,6 +4,7 @@ import static org.folio.des.support.TestUtils.setInternalState; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,7 +27,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -45,10 +45,10 @@ void setUp() { var exportConfigMapperResolver = new ExportConfigMapperResolver(Map.of(), defaultExportConfigMapper); setInternalState(defaultExportConfigMapper, "objectMapper", new JacksonConfiguration().entityObjectMapper()); - repository = Mockito.mock(ExportConfigRepository.class); - bursarExportScheduler = Mockito.mock(BursarExportScheduler.class); + repository = mock(ExportConfigRepository.class); + bursarExportScheduler = mock(BursarExportScheduler.class); service = new BursarFeesFinesExportConfigService(repository, defaultExportConfigMapper, exportConfigMapperResolver, exportConfigValidatorResolver, - Mockito.mock(ExportConfigDomainEventService.class), bursarExportScheduler); + mock(ExportConfigDomainEventService.class), bursarExportScheduler); } @Test diff --git a/src/test/java/org/folio/des/support/TestKafkaConsumer.java b/src/test/java/org/folio/des/support/TestKafkaConsumer.java index 43beb0e0..00b1413c 100644 --- a/src/test/java/org/folio/des/support/TestKafkaConsumer.java +++ b/src/test/java/org/folio/des/support/TestKafkaConsumer.java @@ -136,9 +136,9 @@ public List> drainFor(String key, Duration durati long remaining; try { while ((remaining = deadline - System.nanoTime()) > 0) { - var record = records.poll(remaining, TimeUnit.NANOSECONDS); - if (record != null) { - buffer.add(record); + var polled = records.poll(remaining, TimeUnit.NANOSECONDS); + if (polled != null) { + buffer.add(polled); } } } catch (InterruptedException e) {