test(journeyitem): verify findSummaryByIdInternal never called before JOURNEY-type guard
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m46s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-08 21:38:25 +02:00
parent 84b47f1836
commit 77cbbd34a0
2 changed files with 35 additions and 3 deletions

View File

@@ -1008,9 +1008,20 @@ public class DocumentService {
/**
* Lightweight summary lookup for internal use (e.g. journey item append validation).
* Intentionally skips scope checks and tag-colour resolution — safe only
* under the current single-tenant model where all authenticated users share
* the same document scope. Called within a caller-provided transaction.
*
* <p><strong>Security contract — read before calling:</strong>
* <ol>
* <li>This method intentionally bypasses per-document scope checks and
* tag-colour resolution. It must only be invoked after
* {@code @RequirePermission(BLOG_WRITE)} has already been enforced at
* the controller layer, guaranteeing the caller is an authenticated
* author.</li>
* <li>In {@code JourneyItemService.append()}, it is additionally guarded by the
* JOURNEY-type check that fires before this call — so the method is never
* reached for STORY-type Geschichten.</li>
* </ol>
* Under the current single-tenant model every authenticated author shares the
* same document scope, so skipping per-document scope checks is safe.
*/
public Document findSummaryByIdInternal(UUID id) {
return documentRepository.findById(id)

View File

@@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -235,6 +236,26 @@ class JourneyItemServiceTest {
.isEqualTo(ErrorCode.GESCHICHTE_TYPE_MISMATCH));
}
@Test
void append_never_calls_findSummaryByIdInternal_when_geschichte_type_is_STORY() {
// Arrange: mock geschichteQueryService.findById() to return a STORY-type Geschichte
UUID storyId = UUID.randomUUID();
Geschichte story = Geschichte.builder()
.id(storyId)
.type(GeschichteType.STORY)
.build();
when(geschichteQueryService.findById(storyId)).thenReturn(Optional.of(story));
// Act + Assert: calling append throws GESCHICHTE_TYPE_MISMATCH
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
dto.setDocumentId(UUID.randomUUID());
assertThatThrownBy(() -> journeyItemService.append(storyId, dto))
.isInstanceOf(DomainException.class);
// Verify: document service was never touched — type guard fired first
verifyNoInteractions(documentService);
}
@Test
void append_returns404_when_documentId_does_not_exist() {
Geschichte journey = journey(geschichteId);