feat(document): reject end date without RANGE precision (#678)

Add the second validateDateRange predicate mirroring
chk_meta_date_end_only_for_range, so a direct API client that sets an end
date without RANGE precision gets a clean 400 INVALID_DATE_RANGE instead of
a 500 (AC6). Shares the code with the end-before-start branch.

Also fix updateDocument_preservesStoredPrecision_whenDtoOmitsIt: its stored
fixture (MONTH + end date) is a state the DB CHECK forbids, so the
carried-over-state guard correctly rejects it. Switched to RANGE + end —
the only DB-valid non-null-end combo — preserving the test's intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-01 09:17:52 +02:00
parent 6c5e5273bb
commit 73f614bc3a
2 changed files with 30 additions and 2 deletions

View File

@@ -486,6 +486,12 @@ public class DocumentService {
throw DomainException.badRequest(ErrorCode.INVALID_DATE_RANGE, throw DomainException.badRequest(ErrorCode.INVALID_DATE_RANGE,
"meta_date_end must not be before meta_date"); "meta_date_end must not be before meta_date");
} }
// Mirrors chk_meta_date_end_only_for_range. API-only: the edit form clears the
// end field off-RANGE, so this branch closes the same 500 class for direct clients.
if (doc.getMetaDateEnd() != null && doc.getMetaDatePrecision() != DatePrecision.RANGE) {
throw DomainException.badRequest(ErrorCode.INVALID_DATE_RANGE,
"meta_date_end is only allowed when meta_date_precision is RANGE");
}
} }
@Transactional @Transactional

View File

@@ -204,10 +204,12 @@ class DocumentServiceTest {
// Editing a doc (e.g. fixing a location typo) without touching the precision // Editing a doc (e.g. fixing a location typo) without touching the precision
// controls must NOT fabricate a precision. The form omits the three precision // controls must NOT fabricate a precision. The form omits the three precision
// fields → they arrive null on the DTO → the stored values must be preserved. // fields → they arrive null on the DTO → the stored values must be preserved.
// Stored combo is RANGE + end: the only DB-valid way to have a non-null end
// (chk_meta_date_end_only_for_range), so the carried-over state passes the guard.
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
Document doc = Document.builder() Document doc = Document.builder()
.id(id) .id(id)
.metaDatePrecision(DatePrecision.MONTH) .metaDatePrecision(DatePrecision.RANGE)
.metaDateEnd(LocalDate.of(1916, 6, 30)) .metaDateEnd(LocalDate.of(1916, 6, 30))
.metaDateRaw("Juni 1916") .metaDateRaw("Juni 1916")
.receivers(new HashSet<>()) .receivers(new HashSet<>())
@@ -221,7 +223,7 @@ class DocumentServiceTest {
documentService.updateDocument(id, dto, null, null); documentService.updateDocument(id, dto, null, null);
assertThat(doc.getMetaDatePrecision()).isEqualTo(DatePrecision.MONTH); assertThat(doc.getMetaDatePrecision()).isEqualTo(DatePrecision.RANGE);
assertThat(doc.getMetaDateEnd()).isEqualTo(LocalDate.of(1916, 6, 30)); assertThat(doc.getMetaDateEnd()).isEqualTo(LocalDate.of(1916, 6, 30));
assertThat(doc.getMetaDateRaw()).isEqualTo("Juni 1916"); assertThat(doc.getMetaDateRaw()).isEqualTo("Juni 1916");
} }
@@ -313,6 +315,26 @@ class DocumentServiceTest {
verify(documentRepository, atLeastOnce()).save(any()); verify(documentRepository, atLeastOnce()).save(any());
} }
@Test
void updateDocument_rejectsEndDate_whenPrecisionNotRange() {
// AC6: an end date only makes sense for RANGE (mirrors chk_meta_date_end_only_for_range).
// API-only — the edit form clears the end field off-RANGE — so close the 500 class here too.
UUID id = UUID.randomUUID();
Document doc = docForRangeUpdate(id);
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
DocumentUpdateDTO dto = new DocumentUpdateDTO();
dto.setDocumentDate(LocalDate.of(1917, 1, 10));
dto.setMetaDatePrecision(DatePrecision.MONTH);
dto.setMetaDateEnd(LocalDate.of(1917, 1, 31));
assertThatThrownBy(() -> documentService.updateDocument(id, dto, null, null))
.isInstanceOf(DomainException.class)
.extracting(e -> ((DomainException) e).getCode())
.isEqualTo(ErrorCode.INVALID_DATE_RANGE);
verify(documentRepository, never()).save(any());
}
// ─── deleteTagCascading ─────────────────────────────────────────────────── // ─── deleteTagCascading ───────────────────────────────────────────────────
@Test @Test