diff --git a/aggregation-manager-job/pom.xml b/aggregation-manager-job/pom.xml index f36bfdaae9b567ca491289d2225f7cd633deb8db..3ec206f322539b2f45aefa8ba3738a984d81239c 100644 --- a/aggregation-manager-job/pom.xml +++ b/aggregation-manager-job/pom.xml @@ -50,6 +50,10 @@ <artifactId>ozg-cloud-spring-boot-starter</artifactId> <groupId>de.ozgcloud.api-lib</groupId> </dependency> + <dependency> + <groupId>de.ozgcloud.aggregation</groupId> + <artifactId>aggregation-manager-interface</artifactId> + </dependency> <!-- Spring --> <dependency> <groupId>org.springframework.boot</groupId> diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregation.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregation.java new file mode 100644 index 0000000000000000000000000000000000000000..50368c67cbb3360bdac7474a7f58a5e7089e6d3a --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregation.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation; + +import java.util.stream.Stream; + +import de.ozgcloud.aggregation.warehouse.DocumentEntry; +import lombok.Builder; + +@Builder +public record Aggregation(String aggregationName, Stream<DocumentEntry> documentEntries) { +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoader.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..454b0adc0c7f1e84cf9633d8f15e453ea4ab3e44 --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoader.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation; + +import de.ozgcloud.aggregation.transformation.AggregationMapping; + +public interface AggregationDataLoader { + + void loadIntoTarget(Aggregation aggregation); + + AggregationMapping.Scope getScope(); +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistry.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistry.java new file mode 100644 index 0000000000000000000000000000000000000000..614e35a422db7f486d6fd53a8aaa5ed4878b063d --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistry.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.errorhandling.TechnicalException; + +@Component +public class AggregationDataLoaderRegistry { + + private final Map<AggregationMapping.Scope, AggregationDataLoader> loadersByScope; + + AggregationDataLoaderRegistry(List<AggregationDataLoader> loaders) { + this.loadersByScope = toLoadersByScope(loaders); + } + + public boolean hasLoader(AggregationMapping.Scope scope) { + return loadersByScope.containsKey(scope); + } + + public AggregationDataLoader getLoader(AggregationMapping.Scope scope) { + return Optional.ofNullable(loadersByScope.get(scope)).orElseThrow(() -> new TechnicalException("No data loader for scope " + scope)); + } + + static Map<AggregationMapping.Scope, AggregationDataLoader> toLoadersByScope(List<AggregationDataLoader> loaders) { + try { + return loaders.stream().collect(Collectors.toMap(AggregationDataLoader::getScope, Function.identity())); + } catch (IllegalStateException e) { + throw new TechnicalException("Multiple loaders exist for single scope.", e); + } + } +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerGrpcConfiguration.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerGrpcConfiguration.java index eec32d5d22db5bb94650f73344808f20fc3e5931..3994100601b36455b0cddfbaf5f3dcf965ebfdfa 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerGrpcConfiguration.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerGrpcConfiguration.java @@ -23,9 +23,13 @@ */ package de.ozgcloud.aggregation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import de.ozgcloud.aggregation.data.AggregationDataServiceGrpc; +import de.ozgcloud.aggregation.extern.AggregationDataRemoteService; +import de.ozgcloud.aggregation.extern.GrpcAggregationDataMapper; import de.ozgcloud.apilib.vorgang.OzgCloudVorgangService; import de.ozgcloud.apilib.vorgang.grpc.GrpcOzgCloudVorgangService; import de.ozgcloud.apilib.vorgang.grpc.OzgCloudVorgangMapper; @@ -37,9 +41,13 @@ import net.devh.boot.grpc.client.inject.GrpcClientBean; @Configuration @GrpcClientBean(clazz = VorgangServiceGrpc.VorgangServiceBlockingStub.class, beanName = "vorgangServiceBlockingStub", client = @GrpcClient("vorgang-manager")) +@GrpcClientBean(clazz = AggregationDataServiceGrpc.AggregationDataServiceStub.class, beanName = "aggregationDataServiceStub", client = @GrpcClient("aggregation-manager")) public class AggregationManagerGrpcConfiguration { + @GrpcClient("vorgang-manager") VorgangServiceGrpc.VorgangServiceBlockingStub vorgangServiceBlockingStub; + @GrpcClient("aggregation-manager") + AggregationDataServiceGrpc.AggregationDataServiceStub aggregationDataServiceStub; @Bean OzgCloudVorgangService grpcOzgCloudVorgangService(OzgCloudVorgangMapper vorgangMapper, @@ -49,4 +57,9 @@ public class AggregationManagerGrpcConfiguration { aggregationCallContext); } + @Bean + @ConditionalOnProperty("grpc.client.aggregation-manager.address") + AggregationDataRemoteService aggregationDataRemoteService(AggregationManagerProperties properties, GrpcAggregationDataMapper grpcAggregationDataMapper) { + return new AggregationDataRemoteService(aggregationDataServiceStub, properties, grpcAggregationDataMapper); + } } diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerConfiguration.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerProperties.java similarity index 79% rename from aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerConfiguration.java rename to aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerProperties.java index 00541877a3dbf271f7e375b6abf291d861e785cc..c54be5d11eeba10534f55e0ab8669afcaffb9fa1 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerConfiguration.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerProperties.java @@ -24,15 +24,21 @@ package de.ozgcloud.aggregation; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; import lombok.Getter; import lombok.Setter; +import lombok.extern.log4j.Log4j2; -@ConfigurationProperties(prefix = "aggregation-manager") +@ConfigurationProperties(prefix = "ozgcloud") +@Configuration +@Validated @Getter @Setter -public class AggregationManagerConfiguration { +@Log4j2 +public class AggregationManagerProperties { private int fetchingBatchSize; - + private String mandant; } diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerRunner.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerRunner.java index 3d5596e8748fbdd4c3cf9ebcc884faa7d40fa79b..8b0544e0d812837ccb7969ada028c97c9ee06247 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerRunner.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/AggregationManagerRunner.java @@ -23,8 +23,9 @@ */ package de.ozgcloud.aggregation; -import java.util.Objects; +import java.util.Arrays; import java.util.UUID; +import java.util.stream.Stream; import org.apache.logging.log4j.ThreadContext; import org.springframework.beans.factory.annotation.Lookup; @@ -48,41 +49,50 @@ public abstract class AggregationManagerRunner implements CommandLineRunner { private static final String MDC_EXECUTION = "execution"; - private final AggregationManagerConfiguration config; + private final AggregationManagerProperties aggregationManagerProperties; private final TransformationProperties transformationProperties; private final TransformationService transformationService; + private final AggregationDataLoaderRegistry loaderRegistry; @Override public void run(String... args) throws TransformationException { - var identifier = transformationProperties.getIdentifier(); - var aggregationMappings = transformationProperties.getAggregationMappings(); - if (Objects.isNull(aggregationMappings) || aggregationMappings.isEmpty()) { - runWithDefaultTransformation(transformationService.load(identifier, null)); - } else { - aggregationMappings.stream() - .forEach(aggregationMapping -> runWithTransformation(transformationService.load(identifier, aggregationMapping), - aggregationMapping)); - } + getScopesWithoutConfiguredTransformations().forEach(this::runWithDefaultTransformation); + runWithConfiguredTransformations(); + } + + Stream<AggregationMapping.Scope> getScopesWithoutConfiguredTransformations() { + return Arrays.stream(AggregationMapping.Scope.values()).filter(loaderRegistry::hasLoader).filter(this::hasNoConfiguredTransformations); + } + + boolean hasNoConfiguredTransformations(AggregationMapping.Scope scope) { + return transformationProperties.getAggregationMappings().stream().map(AggregationMapping::getScope).noneMatch(scope::equals); + } + + void runWithDefaultTransformation(AggregationMapping.Scope scope) throws TransformationException { + runWithTransformation( + transformationService.load(transformationProperties.getIdentifier(), null), + createAggregator(scope)); } - void runWithDefaultTransformation(Transformation transformation) { - runWithTransformation(transformation, null); + void runWithConfiguredTransformations() { + transformationProperties.getAggregationMappings().forEach(aggregationMapping -> runWithTransformation( + transformationService.load(transformationProperties.getIdentifier(), aggregationMapping), + createAggregator(aggregationMapping.getScope()).withAggregationMapping(aggregationMapping))); } - void runWithTransformation(Transformation transformation, AggregationMapping aggregationMapping) { + void runWithTransformation(Transformation transformation, Aggregator aggregator) { try (Execution execution = new Execution(transformation)) { ThreadContext.put(MDC_EXECUTION, execution.id.toString()); - prepareAggregator(execution, aggregationMapping).aggregate(); + aggregator.withExecution(execution).aggregate(); } finally { ThreadContext.remove(MDC_EXECUTION); } } - Aggregator prepareAggregator(Execution execution, AggregationMapping aggregationMapping) { + Aggregator createAggregator(AggregationMapping.Scope scope) { return createAggregator() - .withExecution(execution) - .withAggregationMapping(aggregationMapping) - .withBatchSize(config.getFetchingBatchSize()); + .withBatchSize(aggregationManagerProperties.getFetchingBatchSize()) + .withLoader(loaderRegistry.getLoader(scope)); } @Lookup diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregator.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregator.java index 79cf481fe8871d780ceec13bb1b7564037d2fb0c..3049875ba35003057a653cbf82d8019748d8741c 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregator.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/Aggregator.java @@ -22,7 +22,6 @@ import de.ozgcloud.aggregation.transformation.AggregationMapping.FormIdentifier; import de.ozgcloud.aggregation.transformation.TransformationException; import de.ozgcloud.aggregation.transformation.VorgangMapper; import de.ozgcloud.aggregation.warehouse.DocumentEntry; -import de.ozgcloud.aggregation.warehouse.WarehouseRepository; import de.ozgcloud.apilib.vorgang.OzgCloudVorgang; import de.ozgcloud.apilib.vorgang.OzgCloudVorgangQuery; import de.ozgcloud.apilib.vorgang.OzgCloudVorgangQuery.FormIdentification; @@ -42,13 +41,13 @@ class Aggregator { private static final Predicate<Batch> HAS_NEXT_BATCH = x -> !x.items.isEmpty(); private final OzgCloudVorgangService vorgangService; - private final WarehouseRepository repository; private final VorgangMapper vorgangMapper; private Execution execution; private FormIdentifier formIdentifier; - private String collectionName = DocumentEntry.COLLECTION; + private String aggregationName; private int batchSize = 100; + private AggregationDataLoader loader; public Aggregator withExecution(Execution execution) { this.execution = execution; @@ -58,7 +57,7 @@ class Aggregator { public Aggregator withAggregationMapping(AggregationMapping aggregationMapping) { if (Objects.nonNull(aggregationMapping)) { this.formIdentifier = aggregationMapping.getFormIdentifier(); - this.collectionName = aggregationMapping.getName(); + this.aggregationName = aggregationMapping.getName(); } return this; } @@ -68,8 +67,13 @@ class Aggregator { return this; } + public Aggregator withLoader(AggregationDataLoader loader) { + this.loader = loader; + return this; + } + public void aggregate() { - loadVorgaengeIntoRepository(Stream.concat(extractBatchesOfVorgaengeFromDataSource(), extractBatchesOfDeletedVorgaengeFromDataSource())); + loadVorgaenge(Stream.concat(extractBatchesOfVorgaengeFromDataSource(), extractBatchesOfDeletedVorgaengeFromDataSource())); } Stream<Batch> extractBatchesOfVorgaengeFromDataSource() { @@ -129,13 +133,12 @@ class Aggregator { return UUID.randomUUID(); } - void loadVorgaengeIntoRepository(Stream<Batch> batches) { - repository.clearCollection(collectionName); - batches.map(this::transformBatchToDocumentEntries).forEach(this::loadDocumentEntriesIntoRepository); + void loadVorgaenge(Stream<Batch> batches) { + loader.loadIntoTarget(new Aggregation(aggregationName, batches.flatMap(this::transformBatchToDocumentEntries))); } - List<DocumentEntry> transformBatchToDocumentEntries(Batch batch) { - return batch.items.stream().map(vorgang -> transformWithinBatch(batch, vorgang)).filter(Objects::nonNull).toList(); + Stream<DocumentEntry> transformBatchToDocumentEntries(Batch batch) { + return batch.items.stream().map(vorgang -> transformWithinBatch(batch, vorgang)).filter(Objects::nonNull); } DocumentEntry transformWithinBatch(Batch batch, OzgCloudVorgang vorgang) { @@ -149,11 +152,6 @@ class Aggregator { } } - void loadDocumentEntriesIntoRepository(List<DocumentEntry> entries) { - LOG.atDebug().log("store documents: {}", () -> entries.stream().map(DocumentEntry::getId).toList()); - repository.saveAllInCollection(entries, collectionName); - } - record Batch(Execution execution, UUID id, Page page, List<OzgCloudVorgang> items) { } diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java index aadc1cac0a74c6ceec10de99559e7c6862b2a84c..25c70cafbd354f0f5c345c28654a0efa8662de22 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java @@ -23,15 +23,15 @@ */ package de.ozgcloud.aggregation; +import java.util.ArrayList; import java.util.List; -import jakarta.validation.Valid; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; import de.ozgcloud.aggregation.transformation.AggregationMapping; +import jakarta.validation.Valid; import lombok.Getter; import lombok.Setter; import lombok.extern.log4j.Log4j2; @@ -49,7 +49,7 @@ public class TransformationProperties { * field mappings should be applied */ @Valid - private List<AggregationMapping> aggregationMappings; + private List<AggregationMapping> aggregationMappings = new ArrayList<>(); /* * mapping definition for the entry id diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoader.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..81701adf987258c089a22fa88a36fcd8e7436bf4 --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoader.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import java.util.concurrent.ExecutionException; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationDataLoader; +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Component +@ConditionalOnProperty("grpc.client.aggregation-manager.address") +@RequiredArgsConstructor +@Log4j2 +public class AggregationDataRemoteLoader implements AggregationDataLoader { + + private final AggregationDataRemoteService service; + + @Override + public void loadIntoTarget(Aggregation aggregation) { + try { + service.sendAggregationData(aggregation).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TechnicalException("Waiting for sending aggregation data to complete was interrupted.", e); + } catch (ExecutionException e) { + throw new TechnicalException("Error on sending aggregation data.", e); + } + } + + @Override + public AggregationMapping.Scope getScope() { + return AggregationMapping.Scope.EXTERN; + } + +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteService.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..b0b91dcbd5829c257ac1a915ea7a94a9de67a99b --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteService.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.stream.Stream; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationManagerProperties; +import de.ozgcloud.aggregation.data.AggregationDataServiceGrpc; +import de.ozgcloud.aggregation.data.GrpcAggregationData; +import de.ozgcloud.aggregation.data.GrpcSendAggregationDataRequest; +import de.ozgcloud.aggregation.data.GrpcSendAggregationDataResponse; +import de.ozgcloud.aggregation.warehouse.DocumentEntry; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.client.inject.GrpcClient; + +@RequiredArgsConstructor +public class AggregationDataRemoteService { + + @GrpcClient("aggregation-manager") + private final AggregationDataServiceGrpc.AggregationDataServiceStub serviceStub; + private final AggregationManagerProperties properties; + private final GrpcAggregationDataMapper grpcAggregationDataMapper; + + public Future<Void> sendAggregationData(Aggregation aggregation) { + var responseObserver = buildSendAggregationDataResponseObserver(aggregation); + serviceStub.sendAggregationData(responseObserver); + return responseObserver.getResponseFuture(); + } + + SendAggregationDataResponseObserver buildSendAggregationDataResponseObserver(Aggregation aggregation) { + var requestData = new RequestData(properties.getMandant(), aggregation.aggregationName(), + toGrpcAggregationDataStream(aggregation.documentEntries()).iterator()); + return new SendAggregationDataResponseObserver(properties.getFetchingBatchSize(), requestData); + } + + Stream<GrpcAggregationData> toGrpcAggregationDataStream(Stream<DocumentEntry> documentEntries) { + return documentEntries.map(grpcAggregationDataMapper::toGrpcAggregationData); + } + + @RequiredArgsConstructor + static class SendAggregationDataResponseObserver + implements ClientResponseObserver<GrpcSendAggregationDataRequest, GrpcSendAggregationDataResponse> { + + private final int batchSize; + private final RequestData requestData; + @Getter + private final CompletableFuture<Void> responseFuture = new CompletableFuture<>(); + + @Override + public void beforeStart(ClientCallStreamObserver<GrpcSendAggregationDataRequest> requestObserver) { + requestObserver.setOnReadyHandler(buildOnReadyHandler(requestObserver)); + } + + @Override + public void onNext(GrpcSendAggregationDataResponse value) { + + } + + @Override + public void onError(Throwable t) { + responseFuture.completeExceptionally(t); + } + + @Override + public void onCompleted() { + responseFuture.complete(null); + } + + SendAggregationDataOnReadyHandler buildOnReadyHandler(ClientCallStreamObserver<GrpcSendAggregationDataRequest> requestObserver) { + return new SendAggregationDataOnReadyHandler(requestObserver, batchSize, requestData); + } + } + + @RequiredArgsConstructor + static class SendAggregationDataOnReadyHandler implements Runnable { + + private final ClientCallStreamObserver<GrpcSendAggregationDataRequest> requestObserver; + private final int batchSize; + private final RequestData requestData; + + @Override + public void run() { + while (requestObserver.isReady() && requestData.aggregationDataIterator.hasNext()) { + requestObserver.onNext(buildRequest()); + } + if (!requestData.aggregationDataIterator.hasNext()) { + requestObserver.onCompleted(); + } + } + + GrpcSendAggregationDataRequest buildRequest() { + var builder = GrpcSendAggregationDataRequest.newBuilder() + .setName(requestData.aggregationName()) + .setMandant(requestData.mandant()); + addAggregationData(builder); + return builder.build(); + } + + private void addAggregationData(GrpcSendAggregationDataRequest.Builder builder) { + var elementsAdded = 0; + while (requestData.aggregationDataIterator.hasNext() && elementsAdded++ < batchSize) { + builder.addAggregationData(requestData.aggregationDataIterator.next()); + } + } + } + + @Builder + record RequestData(String mandant, String aggregationName, Iterator<GrpcAggregationData> aggregationDataIterator) { + } +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapper.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..33bdee45a8f99724ce130f8be3a95046fe4f4e3e --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import de.ozgcloud.aggregation.data.GrpcAggregationData; +import de.ozgcloud.aggregation.warehouse.DocumentEntry; + +@Mapper +public interface GrpcAggregationDataMapper { + + @Mapping(target = "eingangDatum", source = "eingangsdatum") + @Mapping(target = "vorgangName", source = "vorgangsname") + GrpcAggregationData toGrpcAggregationData(DocumentEntry documentEntry); +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java index f32b019c01de61bd0c2cba4d87b0d2b3e935f3aa..71d00b3d52542aa9a2caf5b0300085cfa804c540 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java @@ -28,7 +28,6 @@ import java.util.List; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - import lombok.Builder; import lombok.Getter; import lombok.Singular; @@ -57,7 +56,7 @@ public class AggregationMapping { } public enum Scope { - LAND, MANDANT; + EXTERN, INTERN; } @Getter diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoader.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..3ef137ad0027ab2416afae8dc81ce81f80a2d43a --- /dev/null +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoader.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.warehouse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationDataLoader; +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class AggregationDataWarehouseLoader implements AggregationDataLoader { + + private final WarehouseRepository repository; + + @Override + public void loadIntoTarget(Aggregation aggregation) { + var collectionName = getCollectionName(aggregation); + repository.clearCollection(collectionName); + repository.saveAllInCollection(aggregation.documentEntries(), collectionName); + } + + String getCollectionName(Aggregation aggregation) { + return StringUtils.isNotBlank(aggregation.aggregationName()) ? aggregation.aggregationName() : DocumentEntry.COLLECTION; + } + + @Override + public AggregationMapping.Scope getScope() { + return AggregationMapping.Scope.INTERN; + } +} diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepository.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepository.java index 0a312165a18ef10eab5ce9178e3c4ba6f4efc396..a2abb1d85de61ec7e9e45170cab347f944c289ba 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepository.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepository.java @@ -1,12 +1,12 @@ package de.ozgcloud.aggregation.warehouse; -import java.util.List; +import java.util.stream.Stream; interface CustomWarehouseRepository { DocumentEntry saveInCollection(DocumentEntry documentEntry, String collectionName); - List<DocumentEntry> saveAllInCollection(Iterable<DocumentEntry> documentEntries, String collectionName); + void saveAllInCollection(Stream<DocumentEntry> documentEntries, String collectionName); void clearCollection(String collectionName); } diff --git a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImpl.java b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImpl.java index a5169d606f6a6055943538d8ac02f8a0477716b1..f2ce884b4e31f245599b2e5c427ef47becc18805 100644 --- a/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImpl.java +++ b/aggregation-manager-job/src/main/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImpl.java @@ -1,7 +1,6 @@ package de.ozgcloud.aggregation.warehouse; -import java.util.List; -import java.util.stream.StreamSupport; +import java.util.stream.Stream; import org.springframework.data.mongodb.core.MongoTemplate; @@ -13,8 +12,8 @@ class CustomWarehouseRepositoryImpl implements CustomWarehouseRepository { private final MongoTemplate mongoTemplate; @Override - public List<DocumentEntry> saveAllInCollection(Iterable<DocumentEntry> documentEntries, String collectionName) { - return StreamSupport.stream(documentEntries.spliterator(), false).map(entry -> saveInCollection(entry, collectionName)).toList(); + public void saveAllInCollection(Stream<DocumentEntry> documentEntries, String collectionName) { + documentEntries.forEach(entry -> saveInCollection(entry, collectionName)); } @Override diff --git a/aggregation-manager-job/src/main/resources/application-local.yml b/aggregation-manager-job/src/main/resources/application-local.yml index f971794e9d5962d901fa845a6421360ae558671a..a038ff206e2a99435dd80fa4a1cfd7df9a427c8e 100644 --- a/aggregation-manager-job/src/main/resources/application-local.yml +++ b/aggregation-manager-job/src/main/resources/application-local.yml @@ -14,10 +14,6 @@ spring: database: aggregation-manager-job ozgcloud: - vorgang-manager: - address: static://127.0.0.1:9090 - negotiationType: PLAINTEXT - -aggregation-manager: - fetching-batch-size: 5 + fetching-batch-size: 5 + mandant: "Landeshauptstadt Kiel" diff --git a/aggregation-manager-job/src/main/resources/application.yml b/aggregation-manager-job/src/main/resources/application.yml index 10ba491e28a6e9b6f87c6acb015035784a31d3fb..879916463132e5cc9e8380de050b048df348bf9d 100644 --- a/aggregation-manager-job/src/main/resources/application.yml +++ b/aggregation-manager-job/src/main/resources/application.yml @@ -39,6 +39,7 @@ spring: application: name: OzgCloud_AggregationManager -aggregation-manager: +ozgcloud: fetching-batch-size: 100 - identifier: "id.value" + aggregation: + identifier: "id.value" diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistryTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..92cde4b15573002348204af710305aa358bce038 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationDataLoaderRegistryTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; + +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.common.test.ReflectionTestUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +class AggregationDataLoaderRegistryTest { + + private final AggregationMapping.Scope registeredScope = AggregationMapping.Scope.INTERN; + private final AggregationMapping.Scope notRegisteredScope = AggregationMapping.Scope.EXTERN; + + private final AggregationDataLoader loader = new TestLoader(registeredScope); + private AggregationDataLoaderRegistry registry; + + @BeforeEach + void init() { + registry = spy(new AggregationDataLoaderRegistry(List.of(loader))); + } + + @Nested + class TestConstructor { + + private final List<AggregationDataLoader> loaders = List.of(new TestLoader(AggregationMapping.Scope.INTERN)); + + @Test + void shouldConvertToLoadersByScope() { + try (MockedStatic<AggregationDataLoaderRegistry> mocked = mockStatic(AggregationDataLoaderRegistry.class)) { + new AggregationDataLoaderRegistry(loaders); + mocked.verify(() -> AggregationDataLoaderRegistry.toLoadersByScope(loaders)); + } + } + + @Test + @SuppressWarnings("unchecked") + void setLoadersByScope() { + var loadersByScope = Map.of(AggregationMapping.Scope.INTERN, new TestLoader(AggregationMapping.Scope.INTERN)); + try (MockedStatic<AggregationDataLoaderRegistry> mocked = mockStatic(AggregationDataLoaderRegistry.class)) { + mocked.when(() -> AggregationDataLoaderRegistry.toLoadersByScope(loaders)).thenReturn(loadersByScope); + + AggregationDataLoaderRegistry registry = new AggregationDataLoaderRegistry(loaders); + + var loadersByScopeFieldValue = ReflectionTestUtils.getField(registry, "loadersByScope", Map.class); + assertThat(loadersByScopeFieldValue).isEqualTo(loadersByScope); + } + } + } + + @Nested + class TestHasLoader { + + @Test + void shouldReturnFalseIfNoLoaderRegisteredForScope() { + var hasLoader = registry.hasLoader(notRegisteredScope); + + assertThat(hasLoader).isFalse(); + } + + @Test + void shouldReturnTrueIfLoaderRegisteredForScope() { + var hasLoader = registry.hasLoader(registeredScope); + + assertThat(hasLoader).isTrue(); + } + } + + @Nested + class TestGetLoader { + + @Test + void shouldReturnLoaderThatSupportsScope() { + var loaderSupportingScope = registry.getLoader(registeredScope); + + assertThat(loaderSupportingScope).isSameAs(loader); + } + + @Test + void shouldThrowExceptionIfNoLoaderRegisteredForScope() { + assertThatThrownBy(() -> registry.getLoader(notRegisteredScope)).isInstanceOf(TechnicalException.class); + } + } + + @Nested + class TestToLoadersByScope { + + @ParameterizedTest + @EnumSource + void shouldAddLoader(AggregationMapping.Scope scope) { + var loader = new TestLoader(scope); + var loadersByScope = AggregationDataLoaderRegistry.toLoadersByScope(List.of(loader)); + + assertThat(loadersByScope).containsEntry(scope, loader); + } + + @Test + void shouldThrowExceptionIfMultipleLoadersHaveSameScope() { + var scope = AggregationMapping.Scope.EXTERN; + assertThatThrownBy(() -> AggregationDataLoaderRegistry.toLoadersByScope(List.of(new TestLoader(scope), new TestLoader(scope)))) + .isInstanceOf(TechnicalException.class); + } + } + + @RequiredArgsConstructor + static class TestLoader implements AggregationDataLoader { + + @Getter + private final AggregationMapping.Scope scope; + + @Override + public void loadIntoTarget(Aggregation aggregation) { + + } + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationManagerRunnerTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationManagerRunnerTest.java index ef2a1f26bb2fdfbcdf47a5dcd7d2f6292f63b661..70483569a3c3da7975005b77bbcd7b67f03091b5 100644 --- a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationManagerRunnerTest.java +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationManagerRunnerTest.java @@ -27,13 +27,16 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.Collections; import java.util.List; +import java.util.stream.Stream; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentMatcher; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -46,24 +49,17 @@ import de.ozgcloud.aggregation.transformation.AggregationMapping; import de.ozgcloud.aggregation.transformation.AggregationMappingTestFactory; import de.ozgcloud.aggregation.transformation.Transformation; import de.ozgcloud.aggregation.transformation.TransformationService; -import de.ozgcloud.aggregation.transformation.VorgangMapper; -import de.ozgcloud.aggregation.warehouse.WarehouseRepository; -import de.ozgcloud.apilib.vorgang.OzgCloudVorgangService; class AggregationManagerRunnerTest { @Mock - private OzgCloudVorgangService vorgangService; - @Mock - private AggregationManagerConfiguration config; + private AggregationManagerProperties aggregationManagerProperties; @Mock private TransformationProperties transformationProperties; @Mock private TransformationService transformationService; @Mock - private WarehouseRepository repository; - @Mock - private VorgangMapper vorgangMapper; + private AggregationDataLoaderRegistry loaderRegistry; @Mock private static Aggregator aggregator; @Spy @@ -72,126 +68,226 @@ class AggregationManagerRunnerTest { @Nested class TestRun { - private final String identifier = LoremIpsum.getInstance().getWords(1); @BeforeEach - void mock() { - when(transformationProperties.getIdentifier()).thenReturn(identifier); - doNothing().when(runner).runWithTransformation(any(), any()); + void init() { + doNothing().when(runner).runWithConfiguredTransformations(); } @Test - void shouldGetAggregationMappings() { + void shouldGetScopesWithoutConfiguredTransformations() { runner.run(); - verify(transformationProperties).getAggregationMappings(); + verify(runner).getScopesWithoutConfiguredTransformations(); + } + + @ParameterizedTest + @EnumSource + void shouldRunWithDefaultTransformationForScopesWithoutConfiguredOnes(AggregationMapping.Scope scope) { + doReturn(Stream.of(scope)).when(runner).getScopesWithoutConfiguredTransformations(); + doNothing().when(runner).runWithDefaultTransformation(any()); + + runner.run(); + + verify(runner).runWithDefaultTransformation(scope); } @Test - void shouldGetIdentifier() { + void shouldRunWithConfiguredTransformations() { runner.run(); - verify(transformationProperties).getIdentifier(); + verify(runner).runWithConfiguredTransformations(); } + } + + @Nested + class TestGetScopesWithoutConfiguredTransformations { + + @ParameterizedTest + @EnumSource + void shouldCheckForLoader(AggregationMapping.Scope scope) { + runner.getScopesWithoutConfiguredTransformations().toList(); - @Nested - class TestOnAggregationMappingConfigured { - private final List<AggregationMapping> aggregationMappings = List.of(AggregationMappingTestFactory.create(), - AggregationMappingTestFactory.create()); + verify(loaderRegistry).hasLoader(scope); + } - @Mock - private Transformation firstTransformation; - @Mock - private Transformation secondTransformation; + @ParameterizedTest + @EnumSource + void shouldCheckForConfiguredTransformationsWithScope(AggregationMapping.Scope scope) { + when(loaderRegistry.hasLoader(any())).thenReturn(true); - @BeforeEach - void mock() { - when(transformationProperties.getAggregationMappings()).thenReturn(aggregationMappings); - when(transformationService.load(any(), any())).thenReturn(firstTransformation, secondTransformation); - } + runner.getScopesWithoutConfiguredTransformations().toList(); - @Test - void shouldLoadTransformationForEachAggregationMapping() { - runner.run(); + verify(runner).hasNoConfiguredTransformations(scope); + } - aggregationMappings.forEach(mapping -> verify(transformationService).load(identifier, mapping)); - } + @ParameterizedTest + @EnumSource + void shouldReturnScopesWithoutTransformations(AggregationMapping.Scope scope) { + doReturn(true).when(runner).hasNoConfiguredTransformations(scope); + doReturn(false).when(runner).hasNoConfiguredTransformations(AdditionalMatchers.not(eq(scope))); + when(loaderRegistry.hasLoader(any())).thenReturn(true); - @Test - void shouldRunWithTransformationForEachTransformation() { - runner.run(); + var scopes = runner.getScopesWithoutConfiguredTransformations(); - verify(runner).runWithTransformation(firstTransformation, aggregationMappings.get(0)); - verify(runner).runWithTransformation(secondTransformation, aggregationMappings.get(1)); - } + assertThat(scopes).containsExactly(scope); } - @Nested - class TestOnAggregationMappingsNull { - @Mock - private Transformation transformation; + @Test + void shouldOmitScopesWithoutLoaders() { + lenient().doReturn(true).when(runner).hasNoConfiguredTransformations(any()); + when(loaderRegistry.hasLoader(any())).thenReturn(false); - @BeforeEach - void mock() { - when(transformationProperties.getAggregationMappings()).thenReturn(null); - when(transformationService.load(any(), any())).thenReturn(transformation); - } + var scopes = runner.getScopesWithoutConfiguredTransformations(); - @Test - void shouldLoadTransformation() { - runner.run(); + assertThat(scopes).isEmpty(); + } + } - verify(transformationService).load(identifier, null); - } + @Nested + class TestHasNoConfiguredTransformations { - @Test - void shouldCallRunWithDefaultTransformation() { - runner.run(); + @Test + void shouldGetAggregationMappings() { + runner.hasNoConfiguredTransformations(AggregationMapping.Scope.INTERN); - verify(runner).runWithDefaultTransformation(transformation); - } + verify(transformationProperties).getAggregationMappings(); } - @Nested - class TestOnAggregationMappingsEmpty { - @Mock - private Transformation transformation; + @Test + void shouldReturnFalseIfThereAreConfiguredTransformations() { + when(transformationProperties.getAggregationMappings()).thenReturn(List.of(AggregationMappingTestFactory.create())); - @BeforeEach - void mock() { - when(transformationProperties.getAggregationMappings()).thenReturn(Collections.emptyList()); - when(transformationService.load(any(), any())).thenReturn(transformation); - } + var hasNoTransformations = runner.hasNoConfiguredTransformations(AggregationMappingTestFactory.SCOPE); - @Test - void shouldLoadTransformation() { - runner.run(); + assertThat(hasNoTransformations).isFalse(); + } - verify(transformationService).load(identifier, null); - } + @Test + void shouldReturnTrueIfThereAreNoConfiguredTransformations() { + when(transformationProperties.getAggregationMappings()).thenReturn(List.of(AggregationMappingTestFactory.createBuilder().scope( + AggregationMapping.Scope.INTERN).build())); - @Test - void shouldCallRunWithDefaultTransformation() { - runner.run(); + var hasNoTransformations = runner.hasNoConfiguredTransformations(AggregationMapping.Scope.EXTERN); - verify(runner).runWithDefaultTransformation(transformation); - } + assertThat(hasNoTransformations).isTrue(); } } @Nested class TestRunWithDefaultTransformation { + private final AggregationMapping.Scope scope = AggregationMapping.Scope.INTERN; + private final String identifier = LoremIpsum.getInstance().getWords(1); + @Mock private Transformation transformation; + @BeforeEach + void init() { + when(transformationProperties.getIdentifier()).thenReturn(identifier); + when(transformationService.load(any(), any())).thenReturn(transformation); + doReturn(aggregator).when(runner).createAggregator(any()); + doNothing().when(runner).runWithTransformation(any(), any()); + } + + @Test + void shouldGetIdentifier() { + runner.runWithDefaultTransformation(scope); + + verify(transformationProperties).getIdentifier(); + } + + @Test + void shouldLoadTransformation() { + runner.runWithDefaultTransformation(scope); + + verify(transformationService).load(identifier, null); + } + + @Test + void shouldCreateAggregatorForScope() { + runner.runWithDefaultTransformation(scope); + + verify(runner).createAggregator(scope); + } + @Test - void shouldCallRunWithTransformation() { + void shouldRunWithTransformation() { + runner.runWithDefaultTransformation(scope); + + verify(runner).runWithTransformation(transformation, aggregator); + } + } + + @Nested + class TestRunWithConfiguredTransformations { + + private final AggregationMapping.Scope firstScope = AggregationMapping.Scope.INTERN; + @Mock + private Transformation firstTransformation; + @Mock + private Aggregator firstAggregator; + + private final AggregationMapping.Scope secondScope = AggregationMapping.Scope.EXTERN; + @Mock + private Transformation secondTransformation; + @Mock + private Aggregator secondAggregator; + + private final String identifier = LoremIpsum.getInstance().getWords(1); + private final List<AggregationMapping> aggregationMappings = List.of( + AggregationMappingTestFactory.createBuilder().scope(firstScope).build(), + AggregationMappingTestFactory.createBuilder().scope(secondScope).build()); + + @BeforeEach + void mock() { + when(transformationProperties.getAggregationMappings()).thenReturn(aggregationMappings); + when(transformationProperties.getIdentifier()).thenReturn(identifier); + when(transformationService.load(any(), any())).thenReturn(firstTransformation, secondTransformation); doNothing().when(runner).runWithTransformation(any(), any()); + doReturn(firstAggregator).when(runner).createAggregator(firstScope); + doReturn(secondAggregator).when(runner).createAggregator(secondScope); + when(firstAggregator.withAggregationMapping(any())).thenReturn(firstAggregator); + when(secondAggregator.withAggregationMapping(any())).thenReturn(secondAggregator); + } + + @Test + void shouldGetIdentifier() { + runner.runWithConfiguredTransformations(); + + verify(transformationProperties, times(2)).getIdentifier(); + } - runner.runWithDefaultTransformation(transformation); + @Test + void shouldLoadTransformationForEachAggregationMapping() { + runner.runWithConfiguredTransformations(); + + aggregationMappings.forEach(mapping -> verify(transformationService).load(identifier, mapping)); + } + + @Test + void shouldCreateAggregatorForScope() { + runner.runWithConfiguredTransformations(); + + verify(runner).createAggregator(firstScope); + verify(runner).createAggregator(secondScope); + } + + @Test + void shouldSetAggregationMappingInAggregator() { + runner.runWithConfiguredTransformations(); + + verify(firstAggregator).withAggregationMapping(aggregationMappings.getFirst()); + verify(secondAggregator).withAggregationMapping(aggregationMappings.getLast()); + } + + @Test + void shouldRunWithTransformationForEachTransformation() { + runner.runWithConfiguredTransformations(); - verify(runner).runWithTransformation(eq(transformation), isNull()); + verify(runner).runWithTransformation(firstTransformation, firstAggregator); + verify(runner).runWithTransformation(secondTransformation, secondAggregator); } } @@ -200,19 +296,18 @@ class AggregationManagerRunnerTest { @Mock private Transformation transformation; - private final AggregationMapping aggregationMapping = AggregationMappingTestFactory.create(); private final ArgumentMatcher<Execution> hasTransformation = execution -> execution.getTransformation().equals(transformation); @BeforeEach - void mock() { - doReturn(aggregator).when(runner).prepareAggregator(any(), any()); + void init() { + when(aggregator.withExecution(any())).thenReturn(aggregator); } @Test - void shouldCallPrepareAggregator() { + void shouldSetExecution() { runWithTransformation(); - verify(runner).prepareAggregator(argThat(hasTransformation), eq(aggregationMapping)); + verify(aggregator).withExecution(argThat(hasTransformation)); } @Test @@ -223,67 +318,67 @@ class AggregationManagerRunnerTest { } private void runWithTransformation() { - runner.runWithTransformation(transformation, aggregationMapping); + runner.runWithTransformation(transformation, aggregator); } } @Nested - class TestPrepareAggregator { + class TestCreateAggregator { - @Mock - private Execution execution; - private final AggregationMapping aggregationMapping = AggregationMappingTestFactory.create(); + private final AggregationMapping.Scope scope = AggregationMapping.Scope.INTERN; private final int batchSize = RandomUtils.insecure().randomInt(); + @Mock + private AggregationDataLoader loader; @BeforeEach - void mock() { - when(config.getFetchingBatchSize()).thenReturn(batchSize); - when(aggregator.withExecution(any())).thenReturn(aggregator); - when(aggregator.withAggregationMapping(any())).thenReturn(aggregator); + void init() { + when(aggregationManagerProperties.getFetchingBatchSize()).thenReturn(batchSize); + when(loaderRegistry.getLoader(any())).thenReturn(loader); when(aggregator.withBatchSize(anyInt())).thenReturn(aggregator); + when(aggregator.withLoader(any())).thenReturn(aggregator); } @Test void shouldGetBatchSize() { - runner.prepareAggregator(execution, aggregationMapping); + runner.createAggregator(scope); - verify(config).getFetchingBatchSize(); + verify(aggregationManagerProperties).getFetchingBatchSize(); } @Test - void shouldSetExecution() { - runner.prepareAggregator(execution, aggregationMapping); + void shouldSetBatchSize() { + runner.createAggregator(scope); - verify(aggregator).withExecution(execution); + verify(aggregator).withBatchSize(batchSize); } @Test - void shouldSetAggregationMapping() { - runner.prepareAggregator(execution, aggregationMapping); + void shouldGetLoaderFromRegistry() { + runner.createAggregator(scope); - verify(aggregator).withAggregationMapping(aggregationMapping); + verify(loaderRegistry).getLoader(scope); } @Test - void shouldSetBatchSize() { - runner.prepareAggregator(execution, aggregationMapping); + void shouldSetLoader() { + runner.createAggregator(scope); - verify(aggregator).withBatchSize(batchSize); + verify(aggregator).withLoader(loader); } @Test void shouldReturnAggregator() { - var result = runner.prepareAggregator(execution, aggregationMapping); + var created = runner.createAggregator(scope); - assertThat(result).isEqualTo(aggregator); + assertThat(created).isEqualTo(aggregator); } } static class AggregationManagerRunnerImpl extends AggregationManagerRunner { - public AggregationManagerRunnerImpl(AggregationManagerConfiguration config, TransformationProperties transformationProperties, - TransformationService transformationService) { - super(config, transformationProperties, transformationService); + public AggregationManagerRunnerImpl(AggregationManagerProperties aggregationManagerProperties, TransformationProperties transformationProperties, + TransformationService transformationService, AggregationDataLoaderRegistry loaderRegistry) { + super(aggregationManagerProperties, transformationProperties, transformationService, loaderRegistry); } @Override diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationTestFactory.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..4f99f08685f79c8f7e4df114648e1f6ee63a9b22 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregationTestFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation; + +import java.util.stream.Stream; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.aggregation.warehouse.DocumentEntry; +import de.ozgcloud.aggregation.warehouse.DocumentEntryTestFactory; + +public class AggregationTestFactory { + + public static final String AGGREGATION_NAME = LoremIpsum.getInstance().getWords(1); + public static final DocumentEntry DOCUMENT_ENTRY = DocumentEntryTestFactory.create(); + + public static Aggregation create() { + return createBuilder().build(); + } + + public static Aggregation.AggregationBuilder createBuilder() { + return Aggregation.builder() + .aggregationName(AGGREGATION_NAME) + .documentEntries(Stream.of(DOCUMENT_ENTRY)); + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregatorTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregatorTest.java index 2d66f74b8f51b0eb4abbd382b62aa780b98dce54..684c5df30efeddec375bc3eecf52e9523b87c23e 100644 --- a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregatorTest.java +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/AggregatorTest.java @@ -82,7 +82,7 @@ class AggregatorTest { private final AggregationMapping aggregationMapping = AggregationMappingTestFactory.create(); private final FormIdentifier formIdentifier = AggregationMappingTestFactory.FORM_IDENTIFIER; - private final String collectionName = AggregationMappingTestFactory.NAME; + private final String aggregationName = AggregationMappingTestFactory.NAME; @Test void shouldReturnSelf() { @@ -99,10 +99,10 @@ class AggregatorTest { } @Test - void shouldSetCollectionName() { + void shouldSetAggregationName() { var result = aggregator.withAggregationMapping(aggregationMapping); - assertThat(ReflectionTestUtils.getField(result, "collectionName")).isEqualTo(collectionName); + assertThat(ReflectionTestUtils.getField(result, "aggregationName")).isEqualTo(aggregationName); } } @@ -124,10 +124,10 @@ class AggregatorTest { } @Test - void shouldNotSetCollectionName() { + void shouldNotSetAggregationName() { var result = aggregator.withAggregationMapping(null); - assertThat(ReflectionTestUtils.getField(result, "collectionName")).isEqualTo(DocumentEntry.COLLECTION); + assertThat(ReflectionTestUtils.getField(result, "aggregationName")).isNull(); } } } @@ -152,6 +152,27 @@ class AggregatorTest { } } + @Nested + class TestWithLoader { + + @Mock + private AggregationDataLoader loader; + + @Test + void shouldReturnSelf() { + var result = aggregator.withLoader(loader); + + assertThat(result).isSameAs(aggregator); + } + + @Test + void shouldSetLoader() { + aggregator.withLoader(loader); + + assertThat(ReflectionTestUtils.getField(aggregator, "loader")).isSameAs(loader); + } + } + @Nested class TestAggregate { @Mock @@ -165,7 +186,7 @@ class AggregatorTest { void setUp() { doReturn(Stream.of(batchOfVorgaenge)).when(aggregator).extractBatchesOfVorgaengeFromDataSource(); doReturn(Stream.of(batchOfDeletedVorgaenge)).when(aggregator).extractBatchesOfDeletedVorgaengeFromDataSource(); - doNothing().when(aggregator).loadVorgaengeIntoRepository(any()); + doNothing().when(aggregator).loadVorgaenge(any()); } @Test @@ -186,7 +207,7 @@ class AggregatorTest { void shouldLoadVorgaengeIntoRepository() { aggregator.aggregate(); - verify(aggregator).loadVorgaengeIntoRepository(batchStreamCaptor.capture()); + verify(aggregator).loadVorgaenge(batchStreamCaptor.capture()); assertThat(batchStreamCaptor.getValue()).containsExactly(batchOfVorgaenge, batchOfDeletedVorgaenge); } } @@ -485,67 +506,55 @@ class AggregatorTest { } @Nested - class TestLoadVorgaengeIntoRepository { + class TestLoadVorgaenge { @Mock private Execution execution; @Mock private Batch batch; + @Mock + private AggregationDataLoader loader; private final DocumentEntry documentEntry = DocumentEntryTestFactory.create(); private final AggregationMapping aggregationMapping = AggregationMappingTestFactory.create(); @Captor - private ArgumentCaptor<List<DocumentEntry>> documentEntriesCaptor; + private ArgumentCaptor<Aggregation> aggregationCaptor; @BeforeEach void init() { - aggregator = aggregator.withAggregationMapping(aggregationMapping); - doReturn(List.of(documentEntry)).when(aggregator).transformBatchToDocumentEntries(any()); - doNothing().when(aggregator).loadDocumentEntriesIntoRepository(any()); + aggregator = aggregator.withAggregationMapping(aggregationMapping).withLoader(loader); } @Test - void shouldDropCollection() { - loadVorgaengeIntoRepository(); + void shouldLoadIntoTarget() { + loadVorgaenge(); - verify(repository).clearCollection(AggregationMappingTestFactory.NAME); + verify(loader).loadIntoTarget(any(Aggregation.class)); } @Test void shouldTransform() { - loadVorgaengeIntoRepository(); + doReturn(Stream.of(documentEntry)).when(aggregator).transformBatchToDocumentEntries(any()); + loadVorgaenge(); + + getArgumentOfLoadIntoTarget().documentEntries().toList(); verify(aggregator).transformBatchToDocumentEntries(batch); } @Test - void shouldLoadIntoRepository() { - loadVorgaengeIntoRepository(); + void shouldSetAggregationName() { + loadVorgaenge(); - verify(aggregator).loadDocumentEntriesIntoRepository(documentEntriesCaptor.capture()); - assertThat(documentEntriesCaptor.getValue()).containsExactly(documentEntry); + assertThat(getArgumentOfLoadIntoTarget().aggregationName()).isEqualTo(AggregationMappingTestFactory.NAME); } - private void loadVorgaengeIntoRepository() { - aggregator.loadVorgaengeIntoRepository(Stream.of(batch)); - } - } - - @Nested - class TestLoadDocumentEntriesIntoRepository { - - private final List<DocumentEntry> documentEntries = List.of(DocumentEntryTestFactory.create()); - private final AggregationMapping aggregationMapping = AggregationMappingTestFactory.create(); - - @BeforeEach - void init() { - aggregator = aggregator.withAggregationMapping(aggregationMapping); + private Aggregation getArgumentOfLoadIntoTarget() { + verify(loader).loadIntoTarget(aggregationCaptor.capture()); + return aggregationCaptor.getValue(); } - @Test - void shouldSaveDocumentEntriesInCollection() { - aggregator.loadDocumentEntriesIntoRepository(documentEntries); - - verify(repository).saveAllInCollection(documentEntries, AggregationMappingTestFactory.NAME); + private void loadVorgaenge() { + aggregator.loadVorgaenge(Stream.of(batch)); } } } diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoaderTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3c7b0b029486e68ae01ee3186ea5e0ae3a99fbf7 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteLoaderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationTestFactory; +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.SneakyThrows; + +class AggregationDataRemoteLoaderTest { + + @Mock + private AggregationDataRemoteService remoteService; + @Spy + @InjectMocks + private AggregationDataRemoteLoader loader; + + @Nested + class TestLoadIntoTarget { + + private final Aggregation aggregation = AggregationTestFactory.create(); + @Mock + private Future<Void> future; + + @BeforeEach + void init() { + when(remoteService.sendAggregationData(any())).thenReturn(future); + } + + @Test + @SneakyThrows + void shouldSendAggregationData() { + loader.loadIntoTarget(aggregation); + + verify(remoteService).sendAggregationData(aggregation); + } + + @Test + @SneakyThrows + void shouldGetFromFuture() { + loader.loadIntoTarget(aggregation); + + verify(future).get(); + } + + @Nested + class TestOnInterruptedException { + + private final InterruptedException exception = new InterruptedException(); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get()).thenThrow(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, () -> loader.loadIntoTarget(aggregation)); + } + + @Test + void shouldInterruptThread() { + try { + loader.loadIntoTarget(aggregation); + } catch (TechnicalException e) { + // expected + } + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } + } + + @Nested + class TestOnExecutionException { + + private final ExecutionException exception = new ExecutionException(new Exception()); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get()).thenThrow(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, () -> loader.loadIntoTarget(aggregation)); + } + } + } + + @Nested + class TestGetScope { + + @Test + void shouldReturnExternScope() { + var scope = loader.getScope(); + + assertThat(scope).isEqualTo(AggregationMapping.Scope.EXTERN); + } + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteServiceTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8d344615e8ba3a5f7876fd24f4a5a1dcaeb04f12 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/AggregationDataRemoteServiceTest.java @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.apache.commons.collections.IteratorUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationManagerProperties; +import de.ozgcloud.aggregation.AggregationTestFactory; +import de.ozgcloud.aggregation.data.AggregationDataServiceGrpc; +import de.ozgcloud.aggregation.data.GrpcAggregationData; +import de.ozgcloud.aggregation.data.GrpcSendAggregationDataRequest; +import de.ozgcloud.aggregation.extern.AggregationDataRemoteService.SendAggregationDataResponseObserver; +import de.ozgcloud.aggregation.warehouse.DocumentEntry; +import de.ozgcloud.common.test.ReflectionTestUtils; +import io.grpc.stub.ClientCallStreamObserver; +import lombok.SneakyThrows; + +class AggregationDataRemoteServiceTest { + + @Mock + private AggregationDataServiceGrpc.AggregationDataServiceStub serviceStub; + @Mock + private AggregationManagerProperties properties; + @Mock + private GrpcAggregationDataMapper grpcAggregationDataMapper; + @InjectMocks + @Spy + private AggregationDataRemoteService service; + + @Nested + class TestSendAggregationData { + + private final Aggregation aggregation = AggregationTestFactory.create(); + + @Mock + private SendAggregationDataResponseObserver responseObserver; + @Mock + private CompletableFuture<Void> responseFuture; + + @BeforeEach + void init() { + doReturn(responseObserver).when(service).buildSendAggregationDataResponseObserver(any()); + when(responseObserver.getResponseFuture()).thenReturn(responseFuture); + } + + @Test + void shouldBuildResponseObserver() { + sendAggregationData(); + + verify(service).buildSendAggregationDataResponseObserver(aggregation); + } + + @Test + void shouldCallServiceStub() { + sendAggregationData(); + + verify(serviceStub).sendAggregationData(responseObserver); + } + + @Test + void shouldReturnFutureFromResponseObserver() { + var future = sendAggregationData(); + + assertThat(future).isSameAs(responseFuture); + } + + private Future<Void> sendAggregationData() { + return service.sendAggregationData(aggregation); + } + } + + @Nested + class TestBuildSendAggregationDataResponseObserver { + + private static final int BATCH_SIZE = new Random().nextInt(100); + @Captor + private ArgumentCaptor<Stream<DocumentEntry>> documentEntriesCaptor; + + @BeforeEach + void init() { + doReturn(Stream.of(GrpcAggregationDataTestFactory.create())).when(service).toGrpcAggregationDataStream(any()); + when(properties.getFetchingBatchSize()).thenReturn(BATCH_SIZE); + } + + @Test + void shouldGetMandant() { + service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + verify(properties).getMandant(); + } + + @Test + void shouldCreateGrpcAggregationDataStream() { + service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + verify(service).toGrpcAggregationDataStream(documentEntriesCaptor.capture()); + assertThat(documentEntriesCaptor.getValue()).containsExactly(AggregationTestFactory.DOCUMENT_ENTRY); + } + + @Test + void shouldGetBatchSize() { + service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + verify(properties).getFetchingBatchSize(); + } + + @Test + void shouldReturnResponseObserver() { + var builtResponseObserver = service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + assertThat(builtResponseObserver).isNotNull(); + } + + @Nested + class TestBuiltResponseObserver { + + private static final String MANDANT = LoremIpsum.getInstance().getWords(1); + + @BeforeEach + void init() { + when(properties.getMandant()).thenReturn(MANDANT); + } + + @Test + void shouldHaveBatchSize() { + var builtResponseObserver = service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + assertThat(getBatchSize(builtResponseObserver)).isEqualTo(BATCH_SIZE); + } + + @Test + void shouldHaveMandant() { + var builtResponseObserver = service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + assertThat(getRequestData(builtResponseObserver).mandant()).isEqualTo(MANDANT); + } + + @Test + void shouldHaveAggregationName() { + var builtResponseObserver = service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + assertThat(getRequestData(builtResponseObserver).aggregationName()).isEqualTo(AggregationTestFactory.AGGREGATION_NAME); + } + + @Test + @SuppressWarnings("unchecked") + void shouldHaveGrpcAggregationData() { + var builtResponseObserver = service.buildSendAggregationDataResponseObserver(AggregationTestFactory.create()); + + var aggregationData = IteratorUtils.toList(getRequestData(builtResponseObserver).aggregationDataIterator()); + assertThat(aggregationData).containsExactly(GrpcAggregationDataTestFactory.create()); + } + + private int getBatchSize(SendAggregationDataResponseObserver responseObserver) { + return ReflectionTestUtils.getField(responseObserver, "batchSize", Integer.class); + } + + private AggregationDataRemoteService.RequestData getRequestData(SendAggregationDataResponseObserver responseObserver) { + return ReflectionTestUtils.getField(responseObserver, "requestData", AggregationDataRemoteService.RequestData.class); + } + } + } + + @Nested + class TestToGrpcAggregationDataStream { + + @BeforeEach + void init() { + when(grpcAggregationDataMapper.toGrpcAggregationData(AggregationTestFactory.DOCUMENT_ENTRY)).thenReturn( + GrpcAggregationDataTestFactory.create()); + } + + @Test + void shouldMapDocumentEntry() { + service.toGrpcAggregationDataStream(Stream.of(AggregationTestFactory.DOCUMENT_ENTRY)).toList(); + + verify(grpcAggregationDataMapper).toGrpcAggregationData(AggregationTestFactory.DOCUMENT_ENTRY); + } + + @Test + void shouldReturnMappedDocumentEntries() { + var mappedDocumentEntries = service.toGrpcAggregationDataStream(Stream.of(AggregationTestFactory.DOCUMENT_ENTRY)); + + assertThat(mappedDocumentEntries.toList()).containsExactly(GrpcAggregationDataTestFactory.create()); + } + } + + @Nested + class SendAggregationDataResponseObserverTest { + + private static final int BATCH_SIZE = 2; + + @Mock + private AggregationDataRemoteService.RequestData requestData; + @Spy + private SendAggregationDataResponseObserver responseObserver = new SendAggregationDataResponseObserver(BATCH_SIZE, requestData); + + @Nested + class TestBeforeStart { + + @Mock + private ClientCallStreamObserver<GrpcSendAggregationDataRequest> requestObserver; + @Mock + private AggregationDataRemoteService.SendAggregationDataOnReadyHandler onReadyHandler; + + @BeforeEach + void init() { + doReturn(onReadyHandler).when(responseObserver).buildOnReadyHandler(any()); + } + + @Test + void shouldBuildOnReadyHandler() { + responseObserver.beforeStart(requestObserver); + + verify(responseObserver).buildOnReadyHandler(requestObserver); + } + + @Test + void shouldSetOnReadyHandler() { + responseObserver.beforeStart(requestObserver); + + verify(requestObserver).setOnReadyHandler(onReadyHandler); + } + } + + @Nested + class TestOnError { + + @Test + @SneakyThrows + void shouldCompleteExceptionally() { + var error = new Throwable(); + + responseObserver.onError(error); + + assertThatException().isThrownBy(() -> responseObserver.getResponseFuture().get()).withCause(error); + } + } + + @Nested + class TestOnCompleted { + + @Test + void shouldCompleteFuture() { + responseObserver.onCompleted(); + + assertThat(responseObserver.getResponseFuture().isDone()).isTrue(); + } + } + } + + @Nested + class SendAggregationDataOnReadyHandlerTest { + + private static final int BATCH_SIZE = 2; + private static final String MANDANT = LoremIpsum.getInstance().getWords(1); + + @Mock + private ClientCallStreamObserver<GrpcSendAggregationDataRequest> requestObserver; + + @Nested + class TestRun { + + @Mock + private GrpcSendAggregationDataRequest request; + + @Test + void shouldCheckIfStreamIsReady() { + createOnReadyHandler(createAggregationData(1)).run(); + + verify(requestObserver).isReady(); + } + + @Test + void shouldNotCallOnNextWhenStreamIsNotReady() { + when(requestObserver.isReady()).thenReturn(false); + + createOnReadyHandler(createAggregationData(1)).run(); + + verify(requestObserver, never()).onNext(any()); + } + + @Test + void shouldNotCallOnNextWhenHasNoMoreData() { + when(requestObserver.isReady()).thenReturn(true); + + createOnReadyHandler(createAggregationData(0)).run(); + + verify(requestObserver, never()).onNext(any()); + } + + @Test + void shouldBuildRequest() { + when(requestObserver.isReady()).thenReturn(true); + var onReadyHandler = spy(createOnReadyHandler(createAggregationData(1))); + + onReadyHandler.run(); + + verify(onReadyHandler).buildRequest(); + } + + @Test + void shouldCallOnNextWithBuiltRequest() { + when(requestObserver.isReady()).thenReturn(true).thenReturn(false); + var onReadyHandler = spy(createOnReadyHandler(createAggregationData(1))); + doReturn(request).when(onReadyHandler).buildRequest(); + + onReadyHandler.run(); + + verify(requestObserver).onNext(request); + } + + @Test + void shouldCallOnNextUntilAllDataWasSent() { + when(requestObserver.isReady()).thenReturn(true); + + createOnReadyHandler(createAggregationData(BATCH_SIZE + 1)).run(); + + verify(requestObserver, times(2)).onNext(any()); + } + + @Test + void shouldCompleteRequest() { + when(requestObserver.isReady()).thenReturn(true); + + createOnReadyHandler(createAggregationData(BATCH_SIZE + 1)).run(); + + verify(requestObserver).onCompleted(); + } + + @Test + void shouldNotCompleteRequestIfNotAllDataWasSent() { + when(requestObserver.isReady()).thenReturn(true).thenReturn(false); + + createOnReadyHandler(createAggregationData(BATCH_SIZE + 1)).run(); + + verify(requestObserver, never()).onCompleted(); + } + } + + @Nested + class TestBuildRequest { + + @Test + void shouldSetName() { + var request = createOnReadyHandler().buildRequest(); + + assertThat(request.getName()).isEqualTo(AggregationTestFactory.AGGREGATION_NAME); + } + + @Test + void shouldSetMandant() { + var request = createOnReadyHandler().buildRequest(); + + assertThat(request.getMandant()).isEqualTo(MANDANT); + } + + @Nested + class OnHasNoAggregationData { + + @Test + void shouldAggregationDataBeEmpty() { + var request = createOnReadyHandler(List.of()).buildRequest(); + + assertThat(request.getAggregationDataCount()).isEqualTo(0); + } + } + + @Nested + class OnHasLessAggregationDataThenBatchSize { + + @Test + void shouldAddAllAggregationData() { + var aggregationData = createAggregationData(BATCH_SIZE - 1); + + var request = createOnReadyHandler(aggregationData).buildRequest(); + + assertThat(request.getAggregationDataList()).hasSameElementsAs(aggregationData); + } + } + + @Nested + class OnHasBatchSizeOfAggregationData { + + @Test + void shouldAddAllAggregationData() { + var aggregationData = createAggregationData(BATCH_SIZE); + + var request = createOnReadyHandler(aggregationData).buildRequest(); + + assertThat(request.getAggregationDataList()).hasSameElementsAs(aggregationData); + } + } + + @Nested + class OnHasMoreAggregationDataThenBatchSize { + + @Test + void shouldHaveExactlyBatchSizeOfAggregationData() { + var aggregationData = createAggregationData(BATCH_SIZE + 1); + + var request = createOnReadyHandler(aggregationData).buildRequest(); + + assertThat(request.getAggregationDataCount()).isEqualTo(BATCH_SIZE); + } + + @Test + void shouldContainOnlyFirstBatchSizeOfElements() { + var aggregationData = createAggregationData(BATCH_SIZE + 1); + + var request = createOnReadyHandler(aggregationData).buildRequest(); + + assertThat(request.getAggregationDataList()).containsExactlyElementsOf(aggregationData.subList(0, BATCH_SIZE)); + } + } + } + + private List<GrpcAggregationData> createAggregationData(int count) { + return IntStream.range(1, count + 1) + .mapToObj(idx -> GrpcAggregationDataTestFactory.createBuilder().setVorgangName("vorgang " + idx).build()) + .toList(); + } + + private AggregationDataRemoteService.SendAggregationDataOnReadyHandler createOnReadyHandler() { + return createOnReadyHandler(List.of()); + } + + private AggregationDataRemoteService.SendAggregationDataOnReadyHandler createOnReadyHandler(List<GrpcAggregationData> aggregationData) { + return new AggregationDataRemoteService.SendAggregationDataOnReadyHandler(requestObserver, BATCH_SIZE, + AggregationDataRemoteService.RequestData.builder() + .mandant(MANDANT) + .aggregationName(AggregationTestFactory.AGGREGATION_NAME) + .aggregationDataIterator(aggregationData.iterator()) + .build()); + } + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapperTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7f552a7853b964f1e4ed94937b2618cbfdb145ec --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataMapperTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import de.ozgcloud.aggregation.data.GrpcObject; +import de.ozgcloud.aggregation.warehouse.DocumentEntryTestFactory; + +class GrpcAggregationDataMapperTest { + + private final GrpcAggregationDataMapper mapper = Mappers.getMapper(GrpcAggregationDataMapper.class); + + @Nested + class TestToGrpcAggregationData { + + @Test + void shouldMapId() { + var grpcData = mapper.toGrpcAggregationData(DocumentEntryTestFactory.create()); + + assertThat(grpcData.getId()).isEqualTo(DocumentEntryTestFactory.ID); + } + + @Test + void shouldMapStatus() { + var grpcData = mapper.toGrpcAggregationData(DocumentEntryTestFactory.create()); + + assertThat(grpcData.getStatus()).isEqualTo(DocumentEntryTestFactory.STATUS); + } + + @Test + void shouldMapEingangDatum() { + var grpcData = mapper.toGrpcAggregationData(DocumentEntryTestFactory.create()); + + assertThat(grpcData.getEingangDatum()).isEqualTo(DocumentEntryTestFactory.EINGANGSDATUM.toString()); + } + + @Test + void shouldMapVorgangName() { + var grpcData = mapper.toGrpcAggregationData(DocumentEntryTestFactory.create()); + + assertThat(grpcData.getVorgangName()).isEqualTo(DocumentEntryTestFactory.VORGANG_NAME); + } + + @Test + void shouldHaveDefaultPayload() { + var grpcData = mapper.toGrpcAggregationData(DocumentEntryTestFactory.create()); + + assertThat(grpcData.getPayload()).isEqualTo(GrpcObject.getDefaultInstance()); + } + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataTestFactory.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..d3ba13a1f713ea51fb2b6e05f1dd18848b6a6619 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/GrpcAggregationDataTestFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import de.ozgcloud.aggregation.data.GrpcAggregationData; +import de.ozgcloud.aggregation.data.GrpcAggregationData.Builder; +import de.ozgcloud.aggregation.warehouse.DocumentEntryTestFactory; + +public class GrpcAggregationDataTestFactory { + + public static GrpcAggregationData create() { + return createBuilder().build(); + } + + public static Builder createBuilder() { + return GrpcAggregationData.newBuilder() + .setId(DocumentEntryTestFactory.ID) + .setStatus(DocumentEntryTestFactory.STATUS) + .setEingangDatum(DocumentEntryTestFactory.EINGANGSDATUM.toString()) + .setVorgangName(DocumentEntryTestFactory.VORGANG_NAME); + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/SpringContextITCase.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/SpringContextITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..b5cd918ad5550a6da6d17ffee03d5d0c613c8777 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/extern/SpringContextITCase.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.extern; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; + +import de.ozgcloud.aggregation.AggregationDataLoader; +import de.ozgcloud.aggregation.AggregationDataLoaderRegistry; +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.test.ITCase; + +class SpringContextITCase { + + @Nested + @ITCase + @TestPropertySource(properties = { "grpc.client.aggregation-manager.address=static://127.0.0.1:9090" }) + class OnAggregationManagerAddressSet { + + @Autowired + private List<AggregationDataLoader> loaders; + @Autowired + private AggregationDataLoaderRegistry registry; + + @Test + void shouldHaveOneLoader() { + assertThat(getLoadersWithScopeExtern(loaders)).singleElement().isInstanceOf(AggregationDataRemoteLoader.class); + } + + @Test + void shouldRegister() { + assertThat(registry.hasLoader(AggregationMapping.Scope.EXTERN)).isTrue(); + } + } + + @Nested + @ITCase + class OnAggregationManagerAddressNotSet { + + @Autowired + private List<AggregationDataLoader> loaders; + @Autowired + private AggregationDataLoaderRegistry registry; + + @Test + void shouldNotHaveLoader() { + assertThat(getLoadersWithScopeExtern(loaders)).isEmpty(); + } + + @Test + void shouldNotRegister() { + assertThat(registry.hasLoader(AggregationMapping.Scope.EXTERN)).isFalse(); + } + } + + private List<AggregationDataLoader> getLoadersWithScopeExtern(List<AggregationDataLoader> loaders) { + return loaders.stream() + .filter(loader -> loader.getScope() == AggregationMapping.Scope.EXTERN) + .toList(); + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java index 44a5c25ce128edf4625a5b95ab27d772bb621c46..8986c7903621e7d0b8fa5282c0a4b36111c6f26d 100644 --- a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java @@ -34,6 +34,7 @@ public class AggregationMappingTestFactory { public static final FormIdentifier FORM_IDENTIFIER = FormIdentifierTestFactory.create(); public static final FieldMapping MAPPING = FieldMappingTestFactory.create(); public static final String NAME = LoremIpsum.getInstance().getWords(1); + public static final AggregationMapping.Scope SCOPE = AggregationMapping.Scope.INTERN; public static AggregationMapping create() { return createBuilder().build(); @@ -43,6 +44,7 @@ public class AggregationMappingTestFactory { return AggregationMapping.builder() .formIdentifier(FORM_IDENTIFIER) .fieldMapping(MAPPING) - .name(NAME); + .name(NAME) + .scope(SCOPE); } } diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoaderTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..13488366a7a590b2f704f80814f332762e8af894 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/AggregationDataWarehouseLoaderTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.warehouse; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.aggregation.Aggregation; +import de.ozgcloud.aggregation.AggregationTestFactory; +import de.ozgcloud.aggregation.transformation.AggregationMapping; + +class AggregationDataWarehouseLoaderTest { + + @Mock + private WarehouseRepository repository; + @Spy + @InjectMocks + private AggregationDataWarehouseLoader loader; + + @Nested + class TestLoadIntoTarget { + + private static final String COLLECTION_NAME = LoremIpsum.getInstance().getWords(1); + private final Aggregation aggregation = AggregationTestFactory.create(); + + @Captor + private ArgumentCaptor<Stream<DocumentEntry>> documentEntriesCaptor; + + @BeforeEach + void init() { + doReturn(COLLECTION_NAME).when(loader).getCollectionName(aggregation); + } + + @Test + void shouldGetCollectionName() { + loader.loadIntoTarget(aggregation); + + verify(loader).getCollectionName(aggregation); + } + + @Test + void shouldClearCollection() { + loader.loadIntoTarget(aggregation); + + verify(repository).clearCollection(COLLECTION_NAME); + } + + @Test + void shouldSaveDocumentEntriesInCollection() { + loader.loadIntoTarget(aggregation); + + verify(repository).saveAllInCollection(documentEntriesCaptor.capture(), eq(COLLECTION_NAME)); + assertThat(documentEntriesCaptor.getValue()).containsExactly(AggregationTestFactory.DOCUMENT_ENTRY); + } + } + + @Nested + class TestGetCollectionName { + + @Test + void shouldReturnAggregationName() { + var collectionName = loader.getCollectionName(AggregationTestFactory.create()); + + assertThat(collectionName).isEqualTo(AggregationTestFactory.AGGREGATION_NAME); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldReturnCollectionNameFromDocumentEntry(String aggregationName) { + var collectionName = loader.getCollectionName(AggregationTestFactory.createBuilder().aggregationName(aggregationName).build()); + + assertThat(collectionName).isEqualTo(DocumentEntry.COLLECTION); + } + } + + @Nested + class TestGetScope { + + @Test + void shouldReturnInternScope() { + var scope = loader.getScope(); + + assertThat(scope).isEqualTo(AggregationMapping.Scope.INTERN); + } + } +} diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImplTest.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImplTest.java index 194a372bc2330c39c339baab737c0bf879fba26e..171257c6467a613678a8ad73542b16308ca77165 100644 --- a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImplTest.java +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/CustomWarehouseRepositoryImplTest.java @@ -1,7 +1,6 @@ package de.ozgcloud.aggregation.warehouse; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.List; @@ -33,20 +32,10 @@ class CustomWarehouseRepositoryImplTest { @Test void shouldCallSaveInCollection() { - repository.saveAllInCollection(documentEntries, collectionName); + repository.saveAllInCollection(documentEntries.stream(), collectionName); verify(repository).saveInCollection(documentEntry, collectionName); } - - @Test - void shouldReturnSavedDocumentEntry() { - var savedDocumentEntry = DocumentEntryTestFactory.create(); - doReturn(savedDocumentEntry).when(repository).saveInCollection(any(), any()); - - var returnedDocumentEntries = repository.saveAllInCollection(documentEntries, collectionName); - - assertThat(returnedDocumentEntries).containsExactly(savedDocumentEntry); - } } @Nested diff --git a/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/SpringContextITCase.java b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/SpringContextITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..5504e78722cfdc81f172c125461de833b5ef3514 --- /dev/null +++ b/aggregation-manager-job/src/test/java/de/ozgcloud/aggregation/warehouse/SpringContextITCase.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.aggregation.warehouse; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import de.ozgcloud.aggregation.AggregationDataLoader; +import de.ozgcloud.aggregation.AggregationDataLoaderRegistry; +import de.ozgcloud.aggregation.transformation.AggregationMapping; +import de.ozgcloud.common.test.ITCase; + +@ITCase +class SpringContextITCase { + + @Autowired + private List<AggregationDataLoader> loaders; + @Autowired + private AggregationDataLoaderRegistry registry; + + @Test + void shouldHaveLoader() { + assertThat(getLoadersWithScopeIntern(loaders)).singleElement().isInstanceOf(AggregationDataWarehouseLoader.class); + } + + @Test + void shouldRegister() { + assertThat(registry.hasLoader(AggregationMapping.Scope.INTERN)).isTrue(); + } + + private List<AggregationDataLoader> getLoadersWithScopeIntern(List<AggregationDataLoader> loaders) { + return loaders.stream() + .filter(loader -> loader.getScope() == AggregationMapping.Scope.INTERN) + .toList(); + } +} diff --git a/aggregation-manager-job/src/test/resources/application-itcase.yml b/aggregation-manager-job/src/test/resources/application-itcase.yml index b370e11fc17d7a792a21b82b7cb7bb69357f3710..c8eef97bb8ab02bde5fc7aed63f23e383d5aea3c 100644 --- a/aggregation-manager-job/src/test/resources/application-itcase.yml +++ b/aggregation-manager-job/src/test/resources/application-itcase.yml @@ -10,14 +10,11 @@ spring: banner-mode: "off" ozgcloud: - vorgang-manager: - address: static://127.0.0.1:9090 - negotiationType: PLAINTEXT command: line: runner: enabled: false - -aggregation-manager: + mandant: "Landeshauptstadt Kiel" fetching-batch-size: 2 - identifier: "id.value" + aggregation: + identifier: "id.value"