From ba174ac98128da254358a5f8dccdfcf8eee93496 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 22:22:21 +0900 Subject: [PATCH 01/11] test(coverage): reject empty JaCoCo production selection --- .../JaCoCoCoverageConfigurationTest.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageConfigurationTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageConfigurationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageConfigurationTest.java new file mode 100644 index 00000000..88043151 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageConfigurationTest.java @@ -0,0 +1,185 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the JaCoCo durable-job coverage gate against an empty production-class selection that can + * otherwise satisfy zero-missed counters vacuously. + */ +class JaCoCoCoverageConfigurationTest { + + private static final Path PROJECT_ROOT = projectRoot(); + private static final Set EXPECTED_CLASS_FILE_PATTERNS = Set.of( + "com/xtrmetl/etl/job/*.class", + "com/xtrmetl/etl/controller/EtlJobController*.class" + ); + + @Test + void reportAndCheckSelectCompiledClassFilesInsteadOfReusingAgentClassNames() throws Exception { + Element plugin = jacocoPlugin(parsePom()); + Element pluginConfiguration = directChild(plugin, "configuration"); + assertTrue( + pluginConfiguration == null || directChild(pluginConfiguration, "includes") == null, + "Plugin-level includes leak one filter syntax into prepare-agent, report, and check" + ); + + assertEquals( + EXPECTED_CLASS_FILE_PATTERNS, + executionIncludes(plugin, "report-durable-job-coverage") + ); + assertEquals( + EXPECTED_CLASS_FILE_PATTERNS, + executionIncludes(plugin, "check-durable-job-coverage") + ); + } + + @Test + void coverageCheckRequiresAtLeastOneAnalyzedProductionClass() throws Exception { + Element plugin = jacocoPlugin(parsePom()); + Element checkExecution = execution(plugin, "check-durable-job-coverage"); + Element configuration = requireDirectChild(checkExecution, "configuration"); + Element rules = requireDirectChild(configuration, "rules"); + Element rule = requireDirectChild(rules, "rule"); + + assertEquals("BUNDLE", directChildText(rule, "element")); + + Element limits = requireDirectChild(rule, "limits"); + boolean foundNonEmptyGuard = false; + for (Element limit : directChildren(limits, "limit")) { + if ("CLASS".equals(directChildText(limit, "counter")) + && "TOTALCOUNT".equals(directChildText(limit, "value")) + && "1".equals(directChildText(limit, "minimum"))) { + foundNonEmptyGuard = true; + } + } + assertTrue( + foundNonEmptyGuard, + "Coverage must fail closed when the selected production bundle contains zero classes" + ); + } + + @Test + void intendedCoverageTargetContainsCompiledProductionClasses() { + Path classes = PROJECT_ROOT.resolve("etl-service/target/classes"); + assertTrue( + Files.isRegularFile(classes.resolve("com/xtrmetl/etl/job/EtlJobService.class")), + "EtlJobService must be a real compiled production coverage target" + ); + assertTrue( + Files.isRegularFile(classes.resolve("com/xtrmetl/etl/controller/EtlJobController.class")), + "EtlJobController must be a real compiled production coverage target" + ); + } + + private static Document parsePom() throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(PROJECT_ROOT.resolve("etl-service/pom.xml").toFile()); + } + + private static Element jacocoPlugin(Document document) { + NodeList plugins = document.getElementsByTagName("plugin"); + for (int index = 0; index < plugins.getLength(); index++) { + Element plugin = (Element) plugins.item(index); + if ("org.jacoco".equals(directChildText(plugin, "groupId")) + && "jacoco-maven-plugin".equals(directChildText(plugin, "artifactId"))) { + return plugin; + } + } + throw new AssertionError("etl-service POM is missing jacoco-maven-plugin"); + } + + private static Set executionIncludes(Element plugin, String executionId) { + Element configuration = requireDirectChild(execution(plugin, executionId), "configuration"); + Element includes = requireDirectChild(configuration, "includes"); + Set values = new LinkedHashSet<>(); + for (Element include : directChildren(includes, "include")) { + values.add(include.getTextContent().trim()); + } + assertFalse(values.isEmpty(), "JaCoCo class-file selection must not be empty"); + return values; + } + + private static Element execution(Element plugin, String executionId) { + Element executions = requireDirectChild(plugin, "executions"); + for (Element execution : directChildren(executions, "execution")) { + if (executionId.equals(directChildText(execution, "id"))) { + return execution; + } + } + throw new AssertionError("Missing JaCoCo execution: " + executionId); + } + + private static Element requireDirectChild(Element parent, String name) { + Element child = directChild(parent, name); + assertNotNull(child, () -> "Missing <" + name + "> under <" + parent.getTagName() + ">"); + return child; + } + + private static Element directChild(Element parent, String name) { + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && name.equals(element.getTagName())) { + return element; + } + } + return null; + } + + private static Set directChildren(Element parent, String name) { + Set matches = new LinkedHashSet<>(); + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && name.equals(element.getTagName())) { + matches.add(element); + } + } + return matches; + } + + private static String directChildText(Element parent, String name) { + Element child = directChild(parent, name); + return child == null ? null : child.getTextContent().trim(); + } + + /** Finds the repository root from root- or module-scoped Maven execution. */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From b65c31adcb636640a1a9a44c5b80eec2a487215c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 22:23:29 +0900 Subject: [PATCH 02/11] fix(coverage): separate JaCoCo class-file filters and reject empty bundles --- etl-service/pom.xml | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/etl-service/pom.xml b/etl-service/pom.xml index 00695de2..3d840a28 100644 --- a/etl-service/pom.xml +++ b/etl-service/pom.xml @@ -98,12 +98,6 @@ org.jacoco jacoco-maven-plugin 0.8.15 - - - com.xtrmetl.etl.job.* - com.xtrmetl.etl.controller.EtlJobController* - - prepare-durable-job-coverage @@ -118,6 +112,12 @@ report + + + com/xtrmetl/etl/job/*.class + com/xtrmetl/etl/controller/EtlJobController*.class + + check-durable-job-coverage @@ -126,14 +126,19 @@ check + + com/xtrmetl/etl/job/*.class + com/xtrmetl/etl/controller/EtlJobController*.class + - CLASS - - com.xtrmetl.etl.job.* - com.xtrmetl.etl.controller.EtlJobController* - + BUNDLE + + CLASS + TOTALCOUNT + 1 + INSTRUCTION MISSEDCOUNT From e9cec632d0b2336040a3e86363dc0348e0cab059 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 22:25:17 +0900 Subject: [PATCH 03/11] docs(coverage): record non-vacuous JaCoCo contract --- docs/doctoring/jacoco-nonvacuous-coverage.md | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/doctoring/jacoco-nonvacuous-coverage.md diff --git a/docs/doctoring/jacoco-nonvacuous-coverage.md b/docs/doctoring/jacoco-nonvacuous-coverage.md new file mode 100644 index 00000000..849473bd --- /dev/null +++ b/docs/doctoring/jacoco-nonvacuous-coverage.md @@ -0,0 +1,96 @@ +# Non-vacuous JaCoCo Coverage Evidence + +**Status:** `active_pr` #164 +**Protected baseline assessed:** `develop@622e5e6c3d534f230c390f10e3832efadfc01825` +**Assessment date:** 2026-08-09 + +## Incident evidence + +PR #155 CI `31314123991`, macOS job `93246494460`, ran the full Maven reactor and emitted: + +```text +--- jacoco:0.8.15:report (report-durable-job-coverage) @ etl-service --- +Analyzed bundle 'etl-service' with 0 classes + +--- jacoco:0.8.15:check (check-durable-job-coverage) @ etl-service --- +All coverage checks have been met. +``` + +The job had already compiled production/test code and ran the real test suites successfully. The coverage defect is therefore not an empty project or missing test execution: the report/check class selection was empty while the configured zero-missed limits still passed. + +That result invalidates the previous use of this JaCoCo execution as evidence of 100% durable-job production coverage. It does not invalidate the test results themselves. + +## Root cause + +Protected `etl-service/pom.xml` configured dotted patterns at plugin scope: + +```text +com.xtrmetl.etl.job.* +com.xtrmetl.etl.controller.EtlJobController* +``` + +JaCoCo `prepare-agent` defines `includes` as class names. `CoverageTransformer` converts those configured names to VM notation before matching loaded classes. + +JaCoCo Maven `report` and `check`, however, define their `includes` as **class files**. `ReportSupport` constructs a Maven `FileFilter` and enumerates matching files below the compiled classes directory before analysis. The plugin-level configuration therefore reused one syntax across execution-time class names and report/check class-file paths. + +The second root cause is control design: every existing coverage limit constrained `MISSEDCOUNT` to zero, but none required the selected bundle to contain a class. An empty bundle can therefore satisfy the limits vacuously. + +## Selected repair + +The active repair separates the goals: + +1. `prepare-agent` receives no restrictive include filter; JaCoCo documents this filter as unnecessary except for technical/performance corner cases. +2. `report-durable-job-coverage` selects compiled files using: + - `com/xtrmetl/etl/job/*.class` + - `com/xtrmetl/etl/controller/EtlJobController*.class` +3. `check-durable-job-coverage` uses the same class-file paths. +4. The check applies limits to the selected `BUNDLE` and first requires `CLASS TOTALCOUNT >= 1`. +5. Exact zero missed INSTRUCTION, LINE, METHOD and BRANCH counters remain unchanged in strictness. + +If this exposes real uncovered production code, the correct repair is additional realistic tests or a reviewed product-code removal/refactor—not reintroducing an empty filter or lowering the thresholds. + +## TDD + +Fail-first commit `ba174ac98128da254358a5f8dccdfcf8eee93496` adds `JaCoCoCoverageConfigurationTest` before changing the POM. The test requires: + +- plugin-level includes absent; +- separate report/check class-file patterns; +- a non-empty BUNDLE guard; +- actual compiled `EtlJobService.class` and `EtlJobController.class` at test runtime. + +Protected POM fails the configuration contract while the named production target classes exist, so the RED reaches the intended quality-gate boundary rather than a missing fixture. + +The first GREEN candidate is `b65c31adcb636640a1a9a44c5b80eec2a487215c`, which changes only JaCoCo goal configuration after the fail-first test. + +## Acceptance evidence + +The final exact head is not accepted until a fresh hosted run proves all of the following: + +- JaCoCo report logs a nonzero analyzed-class count for `etl-service`; +- the generated report includes the intended durable-job classes; +- JaCoCo check satisfies `CLASS TOTALCOUNT >= 1`; +- selected owned production code has zero missed instruction, line, method and branch counters; +- full Maven reactor and supported hosted OS tests succeed; +- any real coverage deficits revealed by the repaired selector are fixed test-first; +- Dependency Review, SBOM, SAST/security and review gates pass; +- current protected synthetic-merge execution is not mislabeled literal-source proof. + +## Rollback + +Reinstating the old plugin-level dotted include patterns is not an acceptable rollback because it restores a proven vacuous coverage gate. If the new selector exposes too much or the target definition is wrong, revise the explicit class-file target under review while retaining the non-empty class-count invariant. + +## References — APA 7th + +JaCoCo. (2026). *Java agent*. https://www.jacoco.org/jacoco/trunk/doc/agent.html + +JaCoCo. (2026). *jacoco:prepare-agent*. https://www.jacoco.org/jacoco/trunk/doc/prepare-agent-mojo.html + +JaCoCo. (2026). *jacoco:report*. https://www.jacoco.org/jacoco/trunk/doc/report-mojo.html + +JaCoCo. (2026). *jacoco:check*. https://www.jacoco.org/jacoco/trunk/doc/check-mojo.html + +JaCoCo. (2026). *CoverageTransformer.java*. https://www.jacoco.org/jacoco/trunk/coverage/org.jacoco.agent.rt/org.jacoco.agent.rt.internal/CoverageTransformer.java.html + +JaCoCo. (2026). *ReportSupport.java*. https://www.jacoco.org/jacoco/trunk/coverage/jacoco-maven-plugin/org.jacoco.maven/ReportSupport.java.html + +JaCoCo. (2026). *FileFilter.java*. https://www.jacoco.org/jacoco/trunk/coverage/jacoco-maven-plugin/org.jacoco.maven/FileFilter.java.html From 6110eaa724d8172c0114918f2bbcab7c4bd340f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 22:57:39 +0900 Subject: [PATCH 04/11] test(coverage): align policy with non-vacuous JaCoCo gate --- .../etl/job/EtlJobCoveragePolicyTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java index 4a728d75..d520b43e 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java @@ -8,19 +8,22 @@ import java.nio.file.Path; import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Keeps the durable-job production slice bound to an executable 100% coverage policy. + * Keeps the durable-job production slice bound to an executable, non-vacuous 100% coverage policy. * *

The policy is intentionally scoped to the production classes introduced by the durable-job - * intake slice. It requires current Java-compatible JaCoCo instrumentation and zero missed - * instructions, lines, methods, or branches while the ordinary {@code mvn test} lifecycle runs.

+ * intake slice. It requires current Java-compatible JaCoCo instrumentation, a non-empty selected + * production bundle, and zero missed instructions, lines, methods, or branches while the ordinary + * {@code mvn test} lifecycle runs.

*/ class EtlJobCoveragePolicyTest { /** - * Requires the ETL module build to fail when any durable-job production path is untested. + * Requires the ETL module build to fail when the selected durable-job bundle is empty or any + * selected durable-job production path is untested. * * @throws IOException when the module build descriptor cannot be read */ @@ -36,10 +39,18 @@ void etlModuleEnforcesCompleteInstructionAndBranchCoverageForTheDurableJobSlice( assertTrue(modulePom.contains("test")); assertTrue(modulePom.contains("report")); assertTrue(modulePom.contains("check")); - assertTrue(modulePom.contains("com.xtrmetl.etl.job.*")); + assertTrue(modulePom.contains("com/xtrmetl/etl/job/*.class")); assertTrue(modulePom.contains( + "com/xtrmetl/etl/controller/EtlJobController*.class" + )); + assertFalse(modulePom.contains("com.xtrmetl.etl.job.*")); + assertFalse(modulePom.contains( "com.xtrmetl.etl.controller.EtlJobController*" )); + assertTrue(modulePom.contains("BUNDLE")); + assertTrue(modulePom.contains("CLASS")); + assertTrue(modulePom.contains("TOTALCOUNT")); + assertTrue(modulePom.contains("1")); assertTrue(modulePom.contains("INSTRUCTION")); assertTrue(modulePom.contains("LINE")); assertTrue(modulePom.contains("METHOD")); From f8fa8504f0cceb2f072befda09eef3d13169b5b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 23:07:31 +0900 Subject: [PATCH 05/11] test(job): cover identifier validation branches --- .../etl/job/EtlJobServiceBoundaryTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java index 6ce140d6..798df104 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java @@ -110,6 +110,10 @@ void rejectsEveryCoveredPayloadAdmissionFailureBeforePersistence() { EtlRequestError.INVALID_JSON, () -> service.submit(null, IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_JSON, + () -> service.submit(" ", IDEMPOTENCY_KEY, "tenant_alpha") + ); assertError( EtlRequestError.INVALID_JSON, () -> service.submit("null", IDEMPOTENCY_KEY, "tenant_alpha") @@ -154,10 +158,50 @@ void rejectsEveryCoveredPayloadAdmissionFailureBeforePersistence() { EtlRequestError.INVALID_RECORD, () -> service.submit("[{\"id\":\" record_alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + payloadWithIdentifier("\u00a0record_alpha"), + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + payloadWithIdentifier("x".repeat(257)), + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); assertError( EtlRequestError.INVALID_RECORD, () -> service.submit("[{\"id\":\"record\\u0000alpha\"}]", IDEMPOTENCY_KEY, "tenant_alpha") ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + payloadWithIdentifier("record" + Character.toString(0x200e) + "alpha"), + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + payloadWithIdentifier("record" + Character.toString(0x2028) + "alpha"), + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); + assertError( + EtlRequestError.INVALID_RECORD, + () -> service.submit( + payloadWithIdentifier("record" + Character.toString(0x2029) + "alpha"), + IDEMPOTENCY_KEY, + "tenant_alpha" + ) + ); assertError( EtlRequestError.INVALID_RECORD, () -> service.submit( @@ -203,6 +247,10 @@ void validatesConstructorAndLookupRequiredValues() { verifyNoInteractions(requestLock, jdbcTemplate); } + private static String payloadWithIdentifier(String identifier) { + return "[{\"id\":\"" + identifier + "\"}]"; + } + private static EtlJobService service( JdbcTemplate jdbcTemplate, EtlRequestLock requestLock, From bc8cb6a422fa805a91904426938d6382faee7791 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 02:38:25 +0900 Subject: [PATCH 06/11] test(job): cover required SHA-256 failure path --- .../etl/job/EtlJobServiceBoundaryTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java index 798df104..2edba652 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java @@ -7,15 +7,19 @@ import com.xtrmetl.etl.service.EtlRequestLock; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -69,6 +73,28 @@ void returnsImmediateConflictWhenAnotherSubmissionOwnsTheTryLock() { verifyNoInteractions(jdbcTemplate); } + @Test + void reportsPlatformFailureWhenRequiredSha256DigestIsUnavailable() + throws NoSuchAlgorithmException { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + EtlJobService service = service(jdbcTemplate, requestLock, new EtlBatchProperties()); + + try (MockedStatic messageDigests = mockStatic(MessageDigest.class)) { + messageDigests.when(() -> MessageDigest.getInstance("SHA-256")) + .thenThrow(new NoSuchAlgorithmException("synthetic coverage probe")); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> service.findOwned(UUID.randomUUID(), "tenant_alpha") + ); + + assertEquals("SHA-256 is required by the Java platform", exception.getMessage()); + assertEquals(NoSuchAlgorithmException.class, exception.getCause().getClass()); + } + verifyNoInteractions(requestLock, jdbcTemplate); + } + @Test void rejectsInvalidKeysAndPrincipalScopesBeforeTransactionOrDatabaseAccess() { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); From 51ec3785461bba8ac12b706122efd47f515db3af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 02:43:08 +0900 Subject: [PATCH 07/11] test(job): exercise SHA-256 provider failure without static mocks --- .../etl/job/EtlJobServiceBoundaryTest.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java index 2edba652..2d254b0b 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java @@ -7,19 +7,23 @@ import com.xtrmetl.etl.service.EtlRequestLock; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -74,15 +78,25 @@ void returnsImmediateConflictWhenAnotherSubmissionOwnsTheTryLock() { } @Test - void reportsPlatformFailureWhenRequiredSha256DigestIsUnavailable() - throws NoSuchAlgorithmException { + void reportsPlatformFailureWhenRequiredSha256DigestIsUnavailable() { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); EtlRequestLock requestLock = mock(EtlRequestLock.class); EtlJobService service = service(jdbcTemplate, requestLock, new EtlBatchProperties()); + Provider[] originalProviders = Security.getProviders(); + Provider[] sha256Providers = Security.getProviders("MessageDigest.SHA-256"); + assertNotNull(sha256Providers); + assertTrue(sha256Providers.length > 0); - try (MockedStatic messageDigests = mockStatic(MessageDigest.class)) { - messageDigests.when(() -> MessageDigest.getInstance("SHA-256")) - .thenThrow(new NoSuchAlgorithmException("synthetic coverage probe")); + Map originalPositions = new HashMap<>(); + for (int index = 0; index < originalProviders.length; index++) { + originalPositions.put(originalProviders[index].getName(), index + 1); + } + + try { + for (Provider provider : sha256Providers) { + Security.removeProvider(provider.getName()); + } + assertThrows(NoSuchAlgorithmException.class, () -> MessageDigest.getInstance("SHA-256")); IllegalStateException exception = assertThrows( IllegalStateException.class, @@ -91,6 +105,12 @@ void reportsPlatformFailureWhenRequiredSha256DigestIsUnavailable() assertEquals("SHA-256 is required by the Java platform", exception.getMessage()); assertEquals(NoSuchAlgorithmException.class, exception.getCause().getClass()); + } finally { + for (Provider provider : originalProviders) { + if (Security.getProvider(provider.getName()) == null) { + Security.insertProviderAt(provider, originalPositions.get(provider.getName())); + } + } } verifyNoInteractions(requestLock, jdbcTemplate); } From bae782c0406bec4258ceadb04251f385980cb99f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 02:50:11 +0900 Subject: [PATCH 08/11] test(job): cover defensive parser branch outcomes --- .../job/EtlJobServiceDefensiveBranchTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceDefensiveBranchTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceDefensiveBranchTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceDefensiveBranchTest.java new file mode 100644 index 00000000..556eb460 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceDefensiveBranchTest.java @@ -0,0 +1,85 @@ +package com.xtrmetl.etl.job; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xtrmetl.etl.service.EtlBatchProperties; +import com.xtrmetl.etl.service.EtlRequestError; +import com.xtrmetl.etl.service.EtlRequestException; +import com.xtrmetl.etl.service.EtlRequestLock; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Exercises defensive JSON-tree outcomes that ordinary Jackson parsing cannot reliably manufacture. + * + *

The service accepts an application-provided {@link ObjectMapper}, copies it, and therefore owns + * the boundary between parser output and durable admission. These tests keep that boundary fail-closed + * when a compatible mapper reports end-of-input without a tree or exposes a Java {@code null} array + * element instead of Jackson's usual {@code NullNode}. Neither case may reach transaction locks or + * persistence.

+ */ +class EtlJobServiceDefensiveBranchTest { + + private static final String IDEMPOTENCY_KEY = "550e8400-e29b-41d4-a716-446655440000"; + private static final String PRINCIPAL_SCOPE = "tenant_alpha"; + + @Test + void rejectsParserEndOfInputWhenMapperProducesNoTree() throws Exception { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + ObjectMapper sourceMapper = mock(ObjectMapper.class); + ObjectMapper copiedMapper = mock(ObjectMapper.class); + when(sourceMapper.copy()).thenReturn(copiedMapper); + when(copiedMapper.readTree("parser-end-of-input")).thenReturn(null); + EtlJobService service = new EtlJobService( + jdbcTemplate, + sourceMapper, + new EtlBatchProperties(), + requestLock + ); + + EtlRequestException exception = assertThrows( + EtlRequestException.class, + () -> service.submit("parser-end-of-input", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) + ); + + assertEquals(EtlRequestError.INVALID_JSON, exception.error()); + verifyNoInteractions(requestLock, jdbcTemplate); + } + + @Test + void rejectsNullParserArrayElementBeforeTransactionOrPersistence() throws Exception { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + EtlRequestLock requestLock = mock(EtlRequestLock.class); + ObjectMapper sourceMapper = mock(ObjectMapper.class); + ObjectMapper copiedMapper = mock(ObjectMapper.class); + JsonNode root = mock(JsonNode.class); + when(sourceMapper.copy()).thenReturn(copiedMapper); + when(copiedMapper.readTree("parser-null-array-element")).thenReturn(root); + when(root.isArray()).thenReturn(true); + when(root.size()).thenReturn(1); + when(root.iterator()).thenReturn(Collections.singletonList((JsonNode) null).iterator()); + EtlJobService service = new EtlJobService( + jdbcTemplate, + sourceMapper, + new EtlBatchProperties(), + requestLock + ); + + EtlRequestException exception = assertThrows( + EtlRequestException.class, + () -> service.submit("parser-null-array-element", IDEMPOTENCY_KEY, PRINCIPAL_SCOPE) + ); + + assertEquals(EtlRequestError.INVALID_RECORD, exception.error()); + verifyNoInteractions(requestLock, jdbcTemplate); + } +} From 9b3410328e8d8e82cea1d4a9c1cd6ef740fde776 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:37:06 +0900 Subject: [PATCH 09/11] docs(coverage): record non-vacuous JaCoCo gate --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b08b8c..82323ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The durable-job JaCoCo gate now selects real compiled production classes per report/check goal and fails closed when the selected bundle is empty, preventing zero-class executions from being reported as 100% owned-production coverage evidence. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. - `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata. - `Idempotency-Key` now prefers the quoted RFC 9651 Structured Field String representation while retaining and normalizing the legacy raw representation to the same durable ledger key. -- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/422/503/500 taxonomy, and no internal exception text in client responses. +- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/503/500 taxonomy, and no internal exception text in client responses. - ETL requests now enforce bounded UTF-8 payload and record-count limits, prevalidate and transform the complete batch before the first JDBC call, and commit accepted records inside one Spring transaction. - ETL transformations now preserve comma/colon-bearing values, use locale-independent text conversion and deterministic `BigDecimal` amount formatting, and retry only transient Spring data-access failures. - Product branding: user-facing docs and suggested image tags use **mightyETL** (formerly xtrmETL). From 2838fbc0735cc5d5d70b254f2eaa793dc995dcd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:37:48 +0900 Subject: [PATCH 10/11] fix(coverage): preserve problem-details changelog taxonomy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82323ea9..b692ca66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. - `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata. - `Idempotency-Key` now prefers the quoted RFC 9651 Structured Field String representation while retaining and normalizing the legacy raw representation to the same durable ledger key. -- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/503/500 taxonomy, and no internal exception text in client responses. +- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/422/503/500 taxonomy, and no internal exception text in client responses. - ETL requests now enforce bounded UTF-8 payload and record-count limits, prevalidate and transform the complete batch before the first JDBC call, and commit accepted records inside one Spring transaction. - ETL transformations now preserve comma/colon-bearing values, use locale-independent text conversion and deterministic `BigDecimal` amount formatting, and retry only transient Spring data-access failures. - Product branding: user-facing docs and suggested image tags use **mightyETL** (formerly xtrmETL). From 54419adebadafebdcf54dc711750fdf24ba8e32e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:19:00 +0900 Subject: [PATCH 11/11] test(coverage): bind zero-missed limits to bundle guard --- .../JaCoCoCoverageRuleCoLocationTest.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageRuleCoLocationTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageRuleCoLocationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageRuleCoLocationTest.java new file mode 100644 index 00000000..4ba745f7 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JaCoCoCoverageRuleCoLocationTest.java @@ -0,0 +1,152 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Verifies that the non-vacuity guard and every zero-missed JaCoCo threshold remain co-located in + * the same {@code BUNDLE} rule. Keeping the limits together prevents a future refactor from leaving + * the non-empty guard on one rule while silently moving coverage thresholds elsewhere. + */ +class JaCoCoCoverageRuleCoLocationTest { + + private static final Set REQUIRED_LIMITS = Set.of( + "CLASS|TOTALCOUNT|min=1", + "INSTRUCTION|MISSEDCOUNT|max=0", + "LINE|MISSEDCOUNT|max=0", + "METHOD|MISSEDCOUNT|max=0", + "BRANCH|MISSEDCOUNT|max=0" + ); + + @Test + void durableJobCoverageKeepsEveryRequiredLimitInOneBundleRule() throws Exception { + Element plugin = jacocoPlugin(parsePom()); + Element execution = execution(plugin, "check-durable-job-coverage"); + Element rules = requireDirectChild(requireDirectChild(execution, "configuration"), "rules"); + Element bundleRule = null; + for (Element rule : directChildren(rules, "rule")) { + if ("BUNDLE".equals(directChildText(rule, "element"))) { + bundleRule = rule; + break; + } + } + assertNotNull(bundleRule, "Coverage check must contain a BUNDLE rule"); + + Set actualLimits = new LinkedHashSet<>(); + for (Element limit : directChildren(requireDirectChild(bundleRule, "limits"), "limit")) { + String counter = directChildText(limit, "counter"); + String value = directChildText(limit, "value"); + String minimum = directChildText(limit, "minimum"); + String maximum = directChildText(limit, "maximum"); + if (minimum != null) { + actualLimits.add(counter + "|" + value + "|min=" + minimum); + } + if (maximum != null) { + actualLimits.add(counter + "|" + value + "|max=" + maximum); + } + } + + assertEquals( + REQUIRED_LIMITS, + actualLimits, + "The BUNDLE rule must keep its non-empty guard and exact zero-missed thresholds together" + ); + } + + private static Document parsePom() throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(projectRoot().resolve("etl-service/pom.xml").toFile()); + } + + private static Element jacocoPlugin(Document document) { + NodeList plugins = document.getElementsByTagName("plugin"); + for (int index = 0; index < plugins.getLength(); index++) { + Element plugin = (Element) plugins.item(index); + if ("org.jacoco".equals(directChildText(plugin, "groupId")) + && "jacoco-maven-plugin".equals(directChildText(plugin, "artifactId"))) { + return plugin; + } + } + throw new AssertionError("etl-service POM is missing jacoco-maven-plugin"); + } + + private static Element execution(Element plugin, String executionId) { + for (Element execution : directChildren(requireDirectChild(plugin, "executions"), "execution")) { + if (executionId.equals(directChildText(execution, "id"))) { + return execution; + } + } + throw new AssertionError("Missing JaCoCo execution: " + executionId); + } + + private static Element requireDirectChild(Element parent, String name) { + Element child = directChild(parent, name); + assertNotNull(child, () -> "Missing <" + name + "> under <" + parent.getTagName() + ">"); + return child; + } + + private static Element directChild(Element parent, String name) { + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && name.equals(element.getTagName())) { + return element; + } + } + return null; + } + + private static Set directChildren(Element parent, String name) { + Set matches = new LinkedHashSet<>(); + NodeList children = parent.getChildNodes(); + for (int index = 0; index < children.getLength(); index++) { + Node child = children.item(index); + if (child instanceof Element element && name.equals(element.getTagName())) { + matches.add(element); + } + } + return matches; + } + + private static String directChildText(Element parent, String name) { + Element child = directChild(parent, name); + return child == null ? null : child.getTextContent().trim(); + } + + /** Finds the repository root from root- or module-scoped Maven execution. */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +}