From 9c46c91aab0f47bd398eca4a08b54822560dd529 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 00:14:51 +0900 Subject: [PATCH 1/5] 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 d6b6759f04c59087db861baefeb0e0cba59e81c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 00:20:58 +0900 Subject: [PATCH 2/5] fix(coverage): make JaCoCo production selection non-vacuous --- 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 a11c0ab2b5047bd55544b2b2e40d21c1258a6115 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 03:05:15 +0900 Subject: [PATCH 3/5] test(coverage): align durable-job policy with non-vacuous bundle gate --- .../etl/job/EtlJobCoveragePolicyTest.java | 16 +++++++++++----- 1 file changed, 11 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..beb63c91 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 @@ -14,13 +14,15 @@ * Keeps the durable-job production slice bound to an executable 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 durable-job production slice is empty or any + * selected production path is untested. * * @throws IOException when the module build descriptor cannot be read */ @@ -36,10 +38,14 @@ 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*" + "com/xtrmetl/etl/controller/EtlJobController*.class" )); + 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 5850abb399a6e0ae2b418965e6542c6967b9bbf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 04:14:35 +0900 Subject: [PATCH 4/5] test(coverage): cover durable-job defensive branches --- .../etl/job/EtlJobServiceBoundaryTest.java | 94 +++++++++++++++++++ .../job/EtlJobServiceDefensiveBranchTest.java | 85 +++++++++++++++++ 2 files changed, 179 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/EtlJobServiceBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobServiceBoundaryTest.java index 6ce140d6..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 @@ -10,10 +10,18 @@ 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.verify; @@ -69,6 +77,44 @@ void returnsImmediateConflictWhenAnotherSubmissionOwnsTheTryLock() { verifyNoInteractions(jdbcTemplate); } + @Test + 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); + + 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, + () -> service.findOwned(UUID.randomUUID(), "tenant_alpha") + ); + + 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); + } + @Test void rejectsInvalidKeysAndPrincipalScopesBeforeTransactionOrDatabaseAccess() { JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); @@ -110,6 +156,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 +204,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 +293,10 @@ void validatesConstructorAndLookupRequiredValues() { verifyNoInteractions(requestLock, jdbcTemplate); } + private static String payloadWithIdentifier(String identifier) { + return "[{\"id\":\"" + identifier + "\"}]"; + } + private static EtlJobService service( JdbcTemplate jdbcTemplate, EtlRequestLock requestLock, 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 1d1f62da4aa2bf86ea547f5bb672168a3e7c34e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 06:12:02 +0900 Subject: [PATCH 5/5] test(coverage): close current durable-job model branches --- .../job/EtlJobModelCoverageBoundaryTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelCoverageBoundaryTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelCoverageBoundaryTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelCoverageBoundaryTest.java new file mode 100644 index 00000000..07405c1c --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelCoverageBoundaryTest.java @@ -0,0 +1,79 @@ +package com.xtrmetl.etl.job; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Closes branch-coverage gaps in immutable durable-job model validation that are part of the + * non-vacuous JaCoCo production slice. + * + *

These cases exercise each distinct short-circuit path in origin-relative status URL and + * lifecycle-dependent failure-code validation. They are intentionally model-boundary tests: no + * database, transaction, network, or controller fixture is needed to prove the record invariants.

+ */ +class EtlJobModelCoverageBoundaryTest { + + private static final UUID JOB_RECORD_ID = UUID.fromString( + "cf4f083f-8c90-4f34-a8b6-b53761de44ef" + ); + private static final Instant CREATED_AT = Instant.parse("2026-08-04T10:00:00Z"); + private static final Instant UPDATED_AT = Instant.parse("2026-08-04T10:00:01Z"); + + @Test + void rejectsEveryUnsafeAcceptedResponseStatusUrlForm() { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + " " + ) + ); + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + "api/etl/jobs/" + JOB_RECORD_ID + ) + ); + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + "//attacker.example/api/etl/jobs/" + JOB_RECORD_ID + ) + ); + } + + @Test + void coversBothFailedStateFailureCodeShortCircuitForms() { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobSnapshot( + JOB_RECORD_ID, + EtlJobStatus.FAILED, + 1, + " ", + CREATED_AT, + UPDATED_AT + ) + ); + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobStatusResponse( + JOB_RECORD_ID, + EtlJobStatus.FAILED, + 1, + null, + CREATED_AT, + UPDATED_AT + ) + ); + } +}