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.setTitle(dto.getTitle());
|
||||||
doc.setDocumentDate(dto.getDocumentDate());
|
doc.setDocumentDate(dto.getDocumentDate());
|
||||||
applyDatePrecision(doc, dto);
|
applyDatePrecision(doc, dto);
|
||||||
|
validateDateRange(doc); // guard before any save (updateDocumentTags below persists)
|
||||||
doc.setLocation(dto.getLocation());
|
doc.setLocation(dto.getLocation());
|
||||||
doc.setTranscription(dto.getTranscription());
|
doc.setTranscription(dto.getTranscription());
|
||||||
doc.setSummary(dto.getSummary());
|
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
|
@Transactional
|
||||||
public Document updateDocumentTags(UUID docId, List<String> tagNames) {
|
public Document updateDocumentTags(UUID docId, List<String> tagNames) {
|
||||||
Document doc = documentRepository.findById(docId)
|
Document doc = documentRepository.findById(docId)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public enum ErrorCode {
|
|||||||
FILE_UPLOAD_FAILED,
|
FILE_UPLOAD_FAILED,
|
||||||
/** The uploaded file's content type is not supported (PDF/JPEG/PNG/TIFF only). 400 */
|
/** The uploaded file's content type is not supported (PDF/JPEG/PNG/TIFF only). 400 */
|
||||||
UNSUPPORTED_FILE_TYPE,
|
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 ---
|
// --- Users ---
|
||||||
/** A user with the given ID or username does not exist. 404 */
|
/** 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.document.SearchMatchData;
|
||||||
import org.raddatz.familienarchiv.tag.TagOperator;
|
import org.raddatz.familienarchiv.tag.TagOperator;
|
||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.document.Document;
|
import org.raddatz.familienarchiv.document.Document;
|
||||||
import org.raddatz.familienarchiv.document.DocumentStatus;
|
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||||
import org.raddatz.familienarchiv.person.Person;
|
import org.raddatz.familienarchiv.person.Person;
|
||||||
@@ -225,6 +226,36 @@ class DocumentServiceTest {
|
|||||||
assertThat(doc.getMetaDateRaw()).isEqualTo("Juni 1916");
|
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 ───────────────────────────────────────────────────
|
// ─── deleteTagCascading ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user