Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/main/java/com/clearfolio/viewer/model/ConversionJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ public ConversionJob(
}

/**
* Creates a conversion job with tenant and subject ownership metadata.
* Creates a conversion job with explicit tenant and subject ownership metadata.
*
* <p>This authority-bearing constructor fails closed when tenant or subject
* claims are absent. Development callers that intentionally use demo
* authority must use one of the convenience constructors that supplies the
* demo identities explicitly.</p>
*
* @param jobId job identifier
* @param tenantId tenant isolation boundary
Expand All @@ -104,8 +109,8 @@ public ConversionJob(
int maxAttempts
) {
this.jobId = jobId;
this.tenantId = normalizeOrDefault(tenantId, DEFAULT_TENANT_ID);
this.subjectId = normalizeOrDefault(subjectId, DEFAULT_SUBJECT_ID);
this.tenantId = requireAuthority(tenantId, "tenantId");
this.subjectId = requireAuthority(subjectId, "subjectId");
this.originalFileName = sanitize(originalFileName);
this.contentType = sanitize(contentType);
this.contentHash = contentHash;
Expand All @@ -125,6 +130,14 @@ private String sanitize(String value) {
return value.replace("\u0000", "");
}

private String requireAuthority(String value, String fieldName) {
String sanitized = sanitize(value);
if (sanitized == null || sanitized.isBlank()) {
throw new IllegalArgumentException(fieldName + " must not be blank");
}
return sanitized.strip();
}

private String normalizeOrDefault(String value, String fallback) {
String sanitized = sanitize(value);
if (sanitized == null || sanitized.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.clearfolio.viewer.model;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.UUID;

import org.junit.jupiter.api.Test;

/**
* Proves that the tenant-aware conversion-job constructor never manufactures
* production authority when explicit tenant or subject claims are absent.
*/
class ConversionJobTenantAuthorityTest {

@Test
void explicitConstructorRejectsMissingTenantAuthority() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new ConversionJob(
UUID.randomUUID(),
null,
"subject-a",
"report.pdf",
"application/pdf",
"hash",
10L,
3
)
);

assertEquals("tenantId must not be blank", exception.getMessage());
}

@Test
void explicitConstructorRejectsBlankTenantAuthorityAfterSanitization() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new ConversionJob(
UUID.randomUUID(),
" \u0000 ",
"subject-a",
"report.pdf",
"application/pdf",
"hash",
10L,
3
)
);

assertEquals("tenantId must not be blank", exception.getMessage());
}

@Test
void explicitConstructorRejectsMissingSubjectAuthority() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new ConversionJob(
UUID.randomUUID(),
"tenant-a",
null,
"report.pdf",
"application/pdf",
"hash",
10L,
3
)
);

assertEquals("subjectId must not be blank", exception.getMessage());
}

@Test
void explicitConstructorRejectsBlankSubjectAuthorityAfterSanitization() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new ConversionJob(
UUID.randomUUID(),
"tenant-a",
" \u0000 ",
"report.pdf",
"application/pdf",
"hash",
10L,
3
)
);

assertEquals("subjectId must not be blank", exception.getMessage());
}
}
20 changes: 2 additions & 18 deletions src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ void constructorSetsDefaultDemoTenantMetadata() {
assertEquals(TenantContext.DEMO_TENANT_ID, job.getTenantId());
assertEquals(TenantContext.DEMO_SUBJECT_ID, job.getSubjectId());
assertTrue(job.belongsToTenant(TenantContext.DEMO_TENANT_ID));
assertFalse(job.belongsToTenant(null));
assertFalse(job.belongsToTenant(" \u0000 "));
}

@Test
Expand All @@ -76,24 +78,6 @@ void constructorAcceptsExplicitTenantMetadata() {
assertFalse(job.belongsToTenant("tenant-b"));
}

@Test
void constructorFallsBackToDemoMetadataForBlankTenantClaims() {
ConversionJob job = new ConversionJob(
UUID.randomUUID(),
" \u0000 ",
null,
"report.docx",
"application/octet-stream",
"hash",
10L,
3
);

assertEquals(TenantContext.DEMO_TENANT_ID, job.getTenantId());
assertEquals(TenantContext.DEMO_SUBJECT_ID, job.getSubjectId());
assertFalse(job.belongsToTenant(null));
}

@Test
void clampsMaxAttemptsToOneWhenZeroIsConfigured() {
ConversionJob job = new ConversionJob(
Expand Down
Loading