From 8caddd09147286792216fcce953e30cda690153e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:20:44 +0900 Subject: [PATCH 1/2] test(etl): prove accepted status URL invariant --- .../EtlJobAcceptedResponseStatusUrlTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobAcceptedResponseStatusUrlTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobAcceptedResponseStatusUrlTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobAcceptedResponseStatusUrlTest.java new file mode 100644 index 00000000..d87e664d --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobAcceptedResponseStatusUrlTest.java @@ -0,0 +1,64 @@ +package com.xtrmetl.etl.job; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies that accepted durable-job representations expose only valid origin-relative status URLs. + */ +class EtlJobAcceptedResponseStatusUrlTest { + + private static final UUID JOB_RECORD_ID = UUID.fromString( + "cf4f083f-8c90-4f34-a8b6-b53761de44ef" + ); + + @Test + void retainsValidOriginRelativeStatusUrl() { + String statusUrl = "/api/etl/jobs/" + JOB_RECORD_ID; + + EtlJobAcceptedResponse response = new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + statusUrl + ); + + assertEquals(statusUrl, response.statusUrl()); + } + + @Test + void rejectsBlankStatusUrls() { + for (String invalidStatusUrl : List.of("", " ", "\t\n")) { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + invalidStatusUrl + ) + ); + } + } + + @Test + void rejectsNonOriginRelativeStatusUrls() { + for (String invalidStatusUrl : List.of( + "https://example.test/api/etl/jobs/" + JOB_RECORD_ID, + "//example.test/api/etl/jobs/" + JOB_RECORD_ID, + "api/etl/jobs/" + JOB_RECORD_ID + )) { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobAcceptedResponse( + JOB_RECORD_ID, + EtlJobStatus.PENDING, + invalidStatusUrl + ) + ); + } + } +} From c8c577b9f538948b145304802b5c5949b1dfe8f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:08:05 +0900 Subject: [PATCH 2/2] fix(etl): enforce accepted status URL invariant --- .../com/xtrmetl/etl/job/EtlJobAcceptedResponse.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobAcceptedResponse.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobAcceptedResponse.java index 5a2ee88a..5258f6c8 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobAcceptedResponse.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobAcceptedResponse.java @@ -8,7 +8,7 @@ * * @param jobRecordId opaque durable job identifier * @param jobStatus current stable job status - * @param statusUrl relative status-monitor resource URL + * @param statusUrl origin-relative status-monitor resource URL beginning with exactly one slash */ public record EtlJobAcceptedResponse( UUID jobRecordId, @@ -21,11 +21,18 @@ public record EtlJobAcceptedResponse( * * @param jobRecordId opaque durable job identifier * @param jobStatus current stable job status - * @param statusUrl relative status-monitor resource URL + * @param statusUrl origin-relative status-monitor resource URL beginning with exactly one slash + * @throws IllegalArgumentException when {@code statusUrl} is blank or is not origin-relative */ public EtlJobAcceptedResponse { Objects.requireNonNull(jobRecordId, "jobRecordId must not be null"); Objects.requireNonNull(jobStatus, "jobStatus must not be null"); Objects.requireNonNull(statusUrl, "statusUrl must not be null"); + if (statusUrl.isBlank()) { + throw new IllegalArgumentException("statusUrl must not be blank"); + } + if (!statusUrl.startsWith("/") || statusUrl.startsWith("//")) { + throw new IllegalArgumentException("statusUrl must be origin-relative"); + } } }