feat(document): reject RANGE with end before start (#678)
Add ErrorCode.INVALID_DATE_RANGE and a validateDateRange guard on DocumentService.updateDocument, run right after applyDatePrecision so it fires before any save (updateDocumentTags persists earlier in the method). Mirrors the V69 chk_meta_date_end_after_start CHECK: end >= start with a null start allowed, using isBefore so equal dates stay valid. Turns a user date typo into a clean 400 instead of a 500 + Sentry alert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,7 @@ public class DocumentService {
|
||||
doc.setTitle(dto.getTitle());
|
||||
doc.setDocumentDate(dto.getDocumentDate());
|
||||
applyDatePrecision(doc, dto);
|
||||
validateDateRange(doc); // guard before any save (updateDocumentTags below persists)
|
||||
doc.setLocation(dto.getLocation());
|
||||
doc.setTranscription(dto.getTranscription());
|
||||
doc.setSummary(dto.getSummary());
|
||||
@@ -468,6 +469,25 @@ public class DocumentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Friendly guard for the two V69 date-range CHECK constraints, run before save so a
|
||||
* user date typo returns a clean 400 INVALID_DATE_RANGE instead of falling through to
|
||||
* the generic handler (HTTP 500 + Sentry + ERROR log). Validates the post-apply {@code doc}
|
||||
* state, not the DTO, because precision/end may have been carried over from the stored row
|
||||
* when the DTO field was null. The DB CHECK remains the backstop; this never weakens it.
|
||||
*/
|
||||
private void validateDateRange(Document doc) {
|
||||
// Mirrors chk_meta_date_end_after_start: end >= start, with null start allowed.
|
||||
// Use isBefore (equal dates are valid) — never !isAfter, which would contradict the DB's >=.
|
||||
if (doc.getMetaDatePrecision() == DatePrecision.RANGE
|
||||
&& doc.getDocumentDate() != null
|
||||
&& doc.getMetaDateEnd() != null
|
||||
&& doc.getMetaDateEnd().isBefore(doc.getDocumentDate())) {
|
||||
throw DomainException.badRequest(ErrorCode.INVALID_DATE_RANGE,
|
||||
"meta_date_end must not be before meta_date");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Document updateDocumentTags(UUID docId, List<String> tagNames) {
|
||||
Document doc = documentRepository.findById(docId)
|
||||
|
||||
@@ -26,6 +26,8 @@ public enum ErrorCode {
|
||||
FILE_UPLOAD_FAILED,
|
||||
/** The uploaded file's content type is not supported (PDF/JPEG/PNG/TIFF only). 400 */
|
||||
UNSUPPORTED_FILE_TYPE,
|
||||
/** A RANGE date is invalid: meta_date_end is before meta_date, or an end date is set without RANGE precision. 400 */
|
||||
INVALID_DATE_RANGE,
|
||||
|
||||
// --- Users ---
|
||||
/** A user with the given ID or username does not exist. 404 */
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.raddatz.familienarchiv.document.MatchOffset;
|
||||
import org.raddatz.familienarchiv.document.SearchMatchData;
|
||||
import org.raddatz.familienarchiv.tag.TagOperator;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.document.Document;
|
||||
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
@@ -225,6 +226,36 @@ class DocumentServiceTest {
|
||||
assertThat(doc.getMetaDateRaw()).isEqualTo("Juni 1916");
|
||||
}
|
||||
|
||||
// ─── updateDocument date-range validation (#678) ──────────────────────────
|
||||
|
||||
/** Builds a stored doc ready for an updateDocument call (collections initialised). */
|
||||
private static Document docForRangeUpdate(UUID id) {
|
||||
return Document.builder().id(id).receivers(new HashSet<>()).tags(new HashSet<>()).build();
|
||||
}
|
||||
|
||||
private static DocumentUpdateDTO rangeDto(LocalDate start, LocalDate end) {
|
||||
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||
dto.setDocumentDate(start);
|
||||
dto.setMetaDatePrecision(DatePrecision.RANGE);
|
||||
dto.setMetaDateEnd(end);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateDocument_rejectsRange_whenEndBeforeStart() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Document doc = docForRangeUpdate(id);
|
||||
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
||||
|
||||
DocumentUpdateDTO dto = rangeDto(LocalDate.of(1917, 1, 11), LocalDate.of(1917, 1, 10));
|
||||
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user