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
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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user