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).
|
* 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
|
* <p><strong>Security contract — read before calling:</strong>
|
||||||
* the same document scope. Called within a caller-provided transaction.
|
* <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) {
|
public Document findSummaryByIdInternal(UUID id) {
|
||||||
return documentRepository.findById(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.lenient;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -235,6 +236,26 @@ class JourneyItemServiceTest {
|
|||||||
.isEqualTo(ErrorCode.GESCHICHTE_TYPE_MISMATCH));
|
.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
|
@Test
|
||||||
void append_returns404_when_documentId_does_not_exist() {
|
void append_returns404_when_documentId_does_not_exist() {
|
||||||
Geschichte journey = journey(geschichteId);
|
Geschichte journey = journey(geschichteId);
|
||||||
|
|||||||
Reference in New Issue
Block a user