diff --git a/cdc-service/src/main/java/com/xtrmetl/cdc/spi/CdcSourceFactory.java b/cdc-service/src/main/java/com/xtrmetl/cdc/spi/CdcSourceFactory.java index d389b166..d9be8af1 100644 --- a/cdc-service/src/main/java/com/xtrmetl/cdc/spi/CdcSourceFactory.java +++ b/cdc-service/src/main/java/com/xtrmetl/cdc/spi/CdcSourceFactory.java @@ -3,10 +3,12 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * Resolves configured CDC source type ids to {@link CdcSourceConnector} instances. @@ -32,15 +34,23 @@ public Optional resolve(String type) { } /** - * Validate a multi-source config list. Does not start engines — live capture - * remains single-source in {@code CdcService}. + * Validates and describes a multi-source configuration list without starting engines. + * Live capture remains single-source in {@code CdcService}. + * + * @param specs configured source entries; {@code null} is treated as an empty list + * @return one descriptive row for each configured source + * @throws IllegalArgumentException when two entries declare the same source id */ public List> describeConfigured(List specs) { List> out = new ArrayList<>(); if (specs == null) { return out; } + Set sourceIds = new HashSet<>(); for (SourceSpec spec : specs) { + if (!sourceIds.add(spec.id())) { + throw new IllegalArgumentException("duplicate source id: " + spec.id()); + } Map row = new java.util.LinkedHashMap<>(); row.put("id", spec.id()); row.put("type", spec.type()); diff --git a/cdc-service/src/test/java/com/xtrmetl/cdc/spi/CdcSourceFactoryIdentityContractTest.java b/cdc-service/src/test/java/com/xtrmetl/cdc/spi/CdcSourceFactoryIdentityContractTest.java new file mode 100644 index 00000000..6bd004b7 --- /dev/null +++ b/cdc-service/src/test/java/com/xtrmetl/cdc/spi/CdcSourceFactoryIdentityContractTest.java @@ -0,0 +1,44 @@ +package com.xtrmetl.cdc.spi; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Protects configured CDC source identity from ambiguous duplicate declarations. + */ +class CdcSourceFactoryIdentityContractTest { + + @Test + void duplicateConfiguredSourceIdsFailClosed() { + CdcSourceFactory factory = factory(); + + IllegalArgumentException failure = assertThrows( + IllegalArgumentException.class, + () -> factory.describeConfigured(List.of( + new CdcSourceFactory.SourceSpec("pg-main", "postgres-debezium", true), + new CdcSourceFactory.SourceSpec("pg-main", "postgres-debezium", false) + )) + ); + + assertTrue(failure.getMessage().contains("pg-main")); + } + + @Test + void repeatedSourceTypeWithDistinctIdsRemainsValid() { + CdcSourceFactory factory = factory(); + + assertDoesNotThrow(() -> factory.describeConfigured(List.of( + new CdcSourceFactory.SourceSpec("pg-primary", "postgres-debezium", true), + new CdcSourceFactory.SourceSpec("pg-secondary", "postgres-debezium", false) + ))); + } + + private static CdcSourceFactory factory() { + return new CdcSourceFactory(new CdcSourceRegistry(List.of(new PostgresDebeziumCdcSource()))); + } +}