From 36ac6676a8616be570747a8e03861090541e1bdc Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:48:34 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=20=EB=A7=A4=ED=95=91=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-buyer-demo.yml 파일 내 불필요하고 취약한 환경변수 주입 매핑 제거 - Spring Boot ConfigTree로 안전하게 주입받도록 강제 --- .../viewer/controller/AdminController.java | 24 ++++++++++++------- src/main/resources/application-buyer-demo.yml | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/controller/AdminController.java b/src/main/java/com/clearfolio/viewer/controller/AdminController.java index 412d4eb86..9d1e52cb9 100644 --- a/src/main/java/com/clearfolio/viewer/controller/AdminController.java +++ b/src/main/java/com/clearfolio/viewer/controller/AdminController.java @@ -25,15 +25,16 @@ @RestController public class AdminController { + /** Conversion service. */ private final DocumentConversionService conversionService; /** * Creates a controller for admin operations. * - * @param conversionService conversion service + * @param cvcService conversion service */ - public AdminController(DocumentConversionService conversionService) { - this.conversionService = conversionService; + public AdminController(final DocumentConversionService cvcService) { + this.conversionService = cvcService; } /** @@ -43,7 +44,8 @@ public AdminController(DocumentConversionService conversionService) { * @return list of conversion jobs */ @GetMapping("/api/v1/admin/convert/jobs") - public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean deadLettered) { + public AdminJobListResponse getAllJobs( + @RequestParam(required = false) final Boolean deadLettered) { Iterable allJobs = conversionService.getAllJobs(); if (deadLettered == null) { @@ -66,7 +68,7 @@ public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean d * @return no content on success */ @DeleteMapping("/api/v1/admin/convert/jobs/{jobId}") - public ResponseEntity deleteJob(@PathVariable UUID jobId) { + public ResponseEntity deleteJob(@PathVariable final UUID jobId) { conversionService.deleteJob(jobId); return ResponseEntity.noContent().build(); } @@ -78,13 +80,17 @@ public ResponseEntity deleteJob(@PathVariable UUID jobId) { * @return accepted response on success */ @PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry") - public ResponseEntity retryDeadLettered(@PathVariable UUID jobId) { - RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin"); + public ResponseEntity retryDeadLettered( + @PathVariable final UUID jobId) { + RetryDeadLetterResult result = conversionService + .retryDeadLettered(jobId, "admin"); if (result == RetryDeadLetterResult.NOT_FOUND) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found"); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "job not found"); } if (result == RetryDeadLetterResult.NOT_ELIGIBLE) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "job is not eligible for retry"); + throw new ResponseStatusException( + HttpStatus.CONFLICT, "job is not eligible for retry"); } return ResponseEntity.accepted().build(); } diff --git a/src/main/resources/application-buyer-demo.yml b/src/main/resources/application-buyer-demo.yml index 4e4df4091..5e0d9a09e 100644 --- a/src/main/resources/application-buyer-demo.yml +++ b/src/main/resources/application-buyer-demo.yml @@ -23,9 +23,9 @@ viewer: clearfolio: artifact-token: - secret: ${CLEARFOLIO_ARTIFACT_TOKEN_SECRET:} + secret: tenant-claims: - hmac-secret: ${CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET:} + hmac-secret: max-skew-seconds: ${CLEARFOLIO_TENANT_CLAIMS_MAX_SKEW_SECONDS:300} artifact-link-ledger: path: ${CLEARFOLIO_ARTIFACT_LINK_LEDGER_PATH:} From f655376d39b4328c605be6eea969e964078ba4ea Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:40:08 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[securi?= =?UTF-8?q?ty=20improvement]=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=20=EB=A7=A4=ED=95=91=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20AdminController=20=EC=9D=B8=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-buyer-demo.yml 파일 내 환경변수 노출을 방지하여 ConfigTree 주입을 강제 - AdminController API(/api/v1/admin/convert/jobs)에 TenantAccessService 인증 체크 추가 - Strix 침투 테스트에서 발견된 AdminController 인증 우회 취약점 해결 --- .../viewer/controller/AdminController.java | 73 ++++++++++++++++--- .../controller/AdminControllerTest.java | 26 ++++++- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/controller/AdminController.java b/src/main/java/com/clearfolio/viewer/controller/AdminController.java index 9d1e52cb9..d78966ffd 100644 --- a/src/main/java/com/clearfolio/viewer/controller/AdminController.java +++ b/src/main/java/com/clearfolio/viewer/controller/AdminController.java @@ -16,6 +16,11 @@ import com.clearfolio.viewer.api.AdminJobListResponse; import com.clearfolio.viewer.model.ConversionJob; +import com.clearfolio.viewer.auth.TenantAccessService; +import com.clearfolio.viewer.auth.TenantContext; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.http.HttpHeaders; +import java.security.GeneralSecurityException; import com.clearfolio.viewer.service.DocumentConversionService; import com.clearfolio.viewer.service.RetryDeadLetterResult; @@ -27,48 +32,70 @@ public class AdminController { /** Conversion service. */ private final DocumentConversionService conversionService; + /** Tenant access service. */ + private final TenantAccessService tenantAccessService; /** * Creates a controller for admin operations. * * @param cvcService conversion service + * @param taService tenant access service */ - public AdminController(final DocumentConversionService cvcService) { + public AdminController( + final DocumentConversionService cvcService, + final TenantAccessService taService) { this.conversionService = cvcService; + this.tenantAccessService = taService; } - /** + /** * Retrieves all conversion jobs, optionally filtered by dead-letter status. * + * @param headers request headers * @param deadLettered optional filter for dead-lettered jobs * @return list of conversion jobs */ @GetMapping("/api/v1/admin/convert/jobs") public AdminJobListResponse getAllJobs( + @RequestHeader final HttpHeaders headers, @RequestParam(required = false) final Boolean deadLettered) { + TenantContext context = tenantAccessService.require( + headers, "admin:read"); Iterable allJobs = conversionService.getAllJobs(); - if (deadLettered == null) { - return AdminJobListResponse.from(allJobs); - } - List filtered = new ArrayList<>(); for (ConversionJob job : allJobs) { - if (job.isDeadLettered() == deadLettered) { + if (!job.belongsToTenant(context.tenantId())) { + continue; + } + if (deadLettered == null || job.isDeadLettered() == deadLettered) { filtered.add(job); } } return AdminJobListResponse.from(filtered); } - /** + /** * Deletes a conversion job. * + * @param headers request headers * @param jobId conversion job identifier * @return no content on success */ @DeleteMapping("/api/v1/admin/convert/jobs/{jobId}") - public ResponseEntity deleteJob(@PathVariable final UUID jobId) { + public ResponseEntity deleteJob( + @RequestHeader final HttpHeaders headers, + @PathVariable final UUID jobId) { + TenantContext context = tenantAccessService.require( + headers, "admin:write"); + ConversionJob job = null; + for (ConversionJob j : conversionService.getAllJobs()) { + if (j.getJobId().equals(jobId)) { + job = j; + break; + } + } + tenantAccessService.requireSameTenant(context, job); conversionService.deleteJob(jobId); return ResponseEntity.noContent().build(); } @@ -76,14 +103,40 @@ public ResponseEntity deleteJob(@PathVariable final UUID jobId) { /** * Retries a dead-lettered conversion job. * + * @param headers request headers * @param jobId conversion job identifier * @return accepted response on success */ @PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry") public ResponseEntity retryDeadLettered( + @RequestHeader final HttpHeaders headers, @PathVariable final UUID jobId) { + TenantContext context = tenantAccessService.require( + headers, "admin:write"); + + ConversionJob job = null; + for (ConversionJob j : conversionService.getAllJobs()) { + if (j.getJobId().equals(jobId)) { + job = j; + break; + } + } + tenantAccessService.requireSameTenant(context, job); + + String actor; + try { + java.security.MessageDigest md = + java.security.MessageDigest.getInstance("SHA-256"); + byte[] bytes = context.subjectId().getBytes( + java.nio.charset.StandardCharsets.UTF_8); + actor = "admin-" + java.util.HexFormat.of().formatHex( + md.digest(bytes)); + } catch (GeneralSecurityException ex) { + throw new IllegalStateException("SHA-256 unavailable", ex); + } + RetryDeadLetterResult result = conversionService - .retryDeadLettered(jobId, "admin"); + .retryDeadLettered(jobId, actor); if (result == RetryDeadLetterResult.NOT_FOUND) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "job not found"); diff --git a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java index ad63a8015..fb8b3b11d 100644 --- a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java @@ -14,19 +14,32 @@ import com.clearfolio.viewer.service.DocumentConversionService; import com.clearfolio.viewer.service.RetryDeadLetterResult; +import com.clearfolio.viewer.auth.TenantAccessService; +import com.clearfolio.viewer.auth.TenantContext; +import org.springframework.http.HttpHeaders; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + class AdminControllerTest { private DocumentConversionService conversionService; + private TenantAccessService tenantAccessService; private WebTestClient webTestClient; private AdminController controller; @BeforeEach void setUp() { conversionService = mock(DocumentConversionService.class); - controller = new AdminController(conversionService); + tenantAccessService = mock(TenantAccessService.class); + controller = new AdminController(conversionService, tenantAccessService); webTestClient = WebTestClient.bindToController(controller) .controllerAdvice(new ApiExceptionHandler()) .build(); + + TenantContext mockContext = mock(TenantContext.class); + when(mockContext.tenantId()).thenReturn("buyer-demo"); + when(mockContext.subjectId()).thenReturn("subject-1"); + when(tenantAccessService.require(any(HttpHeaders.class), any(String.class))).thenReturn(mockContext); } @Test @@ -82,6 +95,8 @@ void getAllJobsFiltersByDeadLetteredFalse() { @Test void deleteJobReturnsNoContent() { UUID jobId = UUID.randomUUID(); + ConversionJob mockJob = new ConversionJob(jobId, "a.pdf", "application/pdf", "hash-a", 100L); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(mockJob)); webTestClient.delete() .uri("/api/v1/admin/convert/jobs/" + jobId) @@ -92,7 +107,8 @@ void deleteJobReturnsNoContent() { @Test void retryDeadLetteredReturnsAcceptedWhenAccepted() { UUID jobId = UUID.randomUUID(); - when(conversionService.retryDeadLettered(jobId, "admin")).thenReturn(RetryDeadLetterResult.ACCEPTED); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(new ConversionJob(jobId, "a", "application/pdf", "hash", 100L))); + when(conversionService.retryDeadLettered(eq(jobId), any(String.class))).thenReturn(RetryDeadLetterResult.ACCEPTED); webTestClient.post() .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") @@ -103,7 +119,8 @@ void retryDeadLetteredReturnsAcceptedWhenAccepted() { @Test void retryDeadLetteredReturnsNotFoundWhenNotFound() { UUID jobId = UUID.randomUUID(); - when(conversionService.retryDeadLettered(jobId, "admin")).thenReturn(RetryDeadLetterResult.NOT_FOUND); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(new ConversionJob(jobId, "a", "application/pdf", "hash", 100L))); + when(conversionService.retryDeadLettered(eq(jobId), any(String.class))).thenReturn(RetryDeadLetterResult.NOT_FOUND); webTestClient.post() .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") @@ -114,7 +131,8 @@ void retryDeadLetteredReturnsNotFoundWhenNotFound() { @Test void retryDeadLetteredReturnsConflictWhenNotEligible() { UUID jobId = UUID.randomUUID(); - when(conversionService.retryDeadLettered(jobId, "admin")).thenReturn(RetryDeadLetterResult.NOT_ELIGIBLE); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(new ConversionJob(jobId, "a", "application/pdf", "hash", 100L))); + when(conversionService.retryDeadLettered(eq(jobId), any(String.class))).thenReturn(RetryDeadLetterResult.NOT_ELIGIBLE); webTestClient.post() .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") From 4eead5fd9d61268b8d6b249a0c172a7b2b7964b1 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:57:50 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20missing=20authentication=20on=20Admin=20endpoints?= =?UTF-8?q?=20and=20secure=20env=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-buyer-demo.yml 파일 내 환경변수 노출을 방지하여 ConfigTree 주입을 강제 - AdminController API(/api/v1/admin/convert/jobs)에 TenantAccessService 인증 체크 추가 - Strix 침투 테스트에서 발견된 AdminController 인증 우회 취약점 해결 --- .../controller/AdminControllerTest.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java index fb8b3b11d..396bf295a 100644 --- a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java @@ -39,6 +39,7 @@ void setUp() { TenantContext mockContext = mock(TenantContext.class); when(mockContext.tenantId()).thenReturn("buyer-demo"); when(mockContext.subjectId()).thenReturn("subject-1"); + org.mockito.Mockito.doCallRealMethod().when(tenantAccessService).requireSameTenant(any(), any()); when(tenantAccessService.require(any(HttpHeaders.class), any(String.class))).thenReturn(mockContext); } @@ -137,6 +138,28 @@ void retryDeadLetteredReturnsConflictWhenNotEligible() { webTestClient.post() .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") .exchange() - .expectStatus().isEqualTo(409); // isConflict() isn't always available depending on spring-test version, so using isEqualTo(409) is safer + .expectStatus().isEqualTo(409); + } + + @Test + void deleteJobThrowsWhenJobNotFound() { + UUID jobId = UUID.randomUUID(); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList()); + + webTestClient.delete() + .uri("/api/v1/admin/convert/jobs/" + jobId) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void retryThrowsWhenJobNotFound() { + UUID jobId = UUID.randomUUID(); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList()); + + webTestClient.post() + .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") + .exchange() + .expectStatus().isNotFound(); } } From 735dafcf3f995be563a04496976c5e59594c1964 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:06:03 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20missing=20authentication=20on=20Admin=20endpoints?= =?UTF-8?q?=20and=20secure=20env=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-buyer-demo.yml 파일 내 환경변수 노출을 방지하여 ConfigTree 주입을 강제 - AdminController API(/api/v1/admin/convert/jobs)에 TenantAccessService 인증 체크 추가 - Strix 침투 테스트에서 발견된 AdminController 인증 우회 취약점 해결 --- .../viewer/controller/AdminController.java | 4 +- .../controller/AdminControllerTest.java | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/clearfolio/viewer/controller/AdminController.java b/src/main/java/com/clearfolio/viewer/controller/AdminController.java index d78966ffd..6d36e54a9 100644 --- a/src/main/java/com/clearfolio/viewer/controller/AdminController.java +++ b/src/main/java/com/clearfolio/viewer/controller/AdminController.java @@ -41,6 +41,8 @@ public class AdminController { * @param cvcService conversion service * @param taService tenant access service */ + + public AdminController( final DocumentConversionService cvcService, final TenantAccessService taService) { @@ -110,7 +112,7 @@ public ResponseEntity deleteJob( @PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry") public ResponseEntity retryDeadLettered( @RequestHeader final HttpHeaders headers, - @PathVariable final UUID jobId) { + @PathVariable final UUID jobId) throws GeneralSecurityException { TenantContext context = tenantAccessService.require( headers, "admin:write"); diff --git a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java index 396bf295a..e66ef7d6d 100644 --- a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java @@ -162,4 +162,41 @@ void retryThrowsWhenJobNotFound() { .exchange() .expectStatus().isNotFound(); } + + @Test + void deleteJobThrowsWhenTenantMismatch() { + UUID jobId = UUID.randomUUID(); + ConversionJob mockJob = new ConversionJob(jobId, "a.pdf", "application/pdf", "hash-a", 100L); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(mockJob)); + + // Use a different mock context that does not match "buyer-demo" + TenantContext mockContext = mock(TenantContext.class); + when(mockContext.tenantId()).thenReturn("other-tenant"); + when(tenantAccessService.require(any(HttpHeaders.class), any(String.class))).thenReturn(mockContext); + + webTestClient.delete() + .uri("/api/v1/admin/convert/jobs/" + jobId) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void retryThrowsWhenTenantMismatch() { + UUID jobId = UUID.randomUUID(); + ConversionJob mockJob = new ConversionJob(jobId, "a.pdf", "application/pdf", "hash-a", 100L); + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(mockJob)); + + TenantContext mockContext = mock(TenantContext.class); + when(mockContext.tenantId()).thenReturn("other-tenant"); + when(mockContext.subjectId()).thenReturn("subject-1"); + when(tenantAccessService.require(any(HttpHeaders.class), any(String.class))).thenReturn(mockContext); + + webTestClient.post() + .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") + .exchange() + .expectStatus().isNotFound(); + } + + + } From 8772377ca256bb1bf7a07caafff47e45321aa5f2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:32:19 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20missing=20authentication=20on=20Admin=20endpoints?= =?UTF-8?q?=20and=20secure=20env=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application-buyer-demo.yml 파일 내 환경변수 노출을 방지하여 ConfigTree 주입을 강제 - AdminController API(/api/v1/admin/convert/jobs)에 TenantAccessService 인증 체크 추가 - Strix 침투 테스트에서 발견된 AdminController 인증 우회 취약점 해결 --- .../viewer/controller/AdminController.java | 16 ++++++--------- .../controller/AdminControllerTest.java | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/controller/AdminController.java b/src/main/java/com/clearfolio/viewer/controller/AdminController.java index 6d36e54a9..b723778f3 100644 --- a/src/main/java/com/clearfolio/viewer/controller/AdminController.java +++ b/src/main/java/com/clearfolio/viewer/controller/AdminController.java @@ -126,16 +126,12 @@ public ResponseEntity retryDeadLettered( tenantAccessService.requireSameTenant(context, job); String actor; - try { - java.security.MessageDigest md = - java.security.MessageDigest.getInstance("SHA-256"); - byte[] bytes = context.subjectId().getBytes( - java.nio.charset.StandardCharsets.UTF_8); - actor = "admin-" + java.util.HexFormat.of().formatHex( - md.digest(bytes)); - } catch (GeneralSecurityException ex) { - throw new IllegalStateException("SHA-256 unavailable", ex); - } + java.security.MessageDigest md = + java.security.MessageDigest.getInstance("SHA-256"); + byte[] bytes = context.subjectId().getBytes( + java.nio.charset.StandardCharsets.UTF_8); + actor = "admin-" + java.util.HexFormat.of() + .formatHex(md.digest(bytes)); RetryDeadLetterResult result = conversionService .retryDeadLettered(jobId, actor); diff --git a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java index e66ef7d6d..d29061e36 100644 --- a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import com.clearfolio.viewer.model.ConversionJobStatus; class AdminControllerTest { private DocumentConversionService conversionService; @@ -93,6 +94,25 @@ void getAllJobsFiltersByDeadLetteredFalse() { .jsonPath("$.jobs[0].fileName").isEqualTo("b.pdf"); } + + @Test + void getAllJobsSkipsJobsNotBelongingToTenant() { + ConversionJob job1 = new ConversionJob(UUID.randomUUID(), "a.pdf", "application/pdf", "hash-a", 100L); + ConversionJob job2 = new ConversionJob(UUID.randomUUID(), "b.pdf", "application/pdf", "hash-b", 100L); + // job1 belongs to buyer-demo by default. + // We can't change job2's tenant easily without reflection or constructor if we didn't add it. + // Wait, ConversionJob has a constructor with tenantId! + ConversionJob job3 = new ConversionJob(UUID.randomUUID(), "other-tenant", "subject-1", "c.pdf", "application/pdf", "hash-c", 100L, 0); + + when(conversionService.getAllJobs()).thenReturn(Arrays.asList(job1, job2, job3)); + + webTestClient.get() + .uri("/api/v1/admin/convert/jobs") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.jobs.length()").isEqualTo(2); + } @Test void deleteJobReturnsNoContent() { UUID jobId = UUID.randomUUID();