fix(ocr): validate personId in TriggerSenderTrainingDTO — returns 400 not 500 on null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 08:49:17 +02:00
parent 8acb830649
commit b879d28761
3 changed files with 82 additions and 2 deletions

View File

@@ -155,7 +155,7 @@ public class OcrController {
@PostMapping("/api/ocr/train-sender")
@ResponseStatus(HttpStatus.ACCEPTED)
@RequirePermission(Permission.ADMIN)
public OcrTrainingRun triggerSenderTraining(@RequestBody TriggerSenderTrainingDTO dto) {
public OcrTrainingRun triggerSenderTraining(@Valid @RequestBody TriggerSenderTrainingDTO dto) {
OcrTrainingRun run = senderModelService.triggerManualSenderTraining(dto.personId());
if (run.getStatus() == TrainingStatus.RUNNING) {
senderModelService.runSenderTraining(dto.personId());

View File

@@ -1,5 +1,12 @@
package org.raddatz.familienarchiv.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
public record TriggerSenderTrainingDTO(UUID personId) {}
public record TriggerSenderTrainingDTO(
@NotNull
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
UUID personId
) {}

View File

@@ -356,6 +356,79 @@ class OcrControllerTest {
.andExpect(jsonPath("$.personNames." + personId).value("Anna Müller"));
}
// ─── POST /api/ocr/train-sender ───────────────────────────────────────────
@Test
@WithMockUser(authorities = "ADMIN")
void triggerSenderTraining_returns400_whenPersonIdIsNull() throws Exception {
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":null}"))
.andExpect(status().isBadRequest());
}
@Test
void triggerSenderTraining_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":\"" + UUID.randomUUID() + "\"}"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(authorities = "READ_ALL")
void triggerSenderTraining_returns403_whenNotAdmin() throws Exception {
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":\"" + UUID.randomUUID() + "\"}"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "ADMIN")
void triggerSenderTraining_returns404_whenPersonNotFound() throws Exception {
UUID unknownId = UUID.randomUUID();
when(senderModelService.triggerManualSenderTraining(unknownId))
.thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found"));
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":\"" + unknownId + "\"}"))
.andExpect(status().isNotFound());
}
@Test
@WithMockUser(authorities = "ADMIN")
void triggerSenderTraining_returns202_withRunning_run() throws Exception {
UUID personId = UUID.randomUUID();
OcrTrainingRun run = OcrTrainingRun.builder()
.id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
.personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build();
when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run);
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":\"" + personId + "\"}"))
.andExpect(status().isAccepted())
.andExpect(jsonPath("$.status").value("RUNNING"));
}
@Test
@WithMockUser(authorities = "ADMIN")
void triggerSenderTraining_returns202_withQueued_run_whenAnotherRunning() throws Exception {
UUID personId = UUID.randomUUID();
OcrTrainingRun run = OcrTrainingRun.builder()
.id(UUID.randomUUID()).status(TrainingStatus.QUEUED)
.personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build();
when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run);
mockMvc.perform(post("/api/ocr/train-sender")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"personId\":\"" + personId + "\"}"))
.andExpect(status().isAccepted())
.andExpect(jsonPath("$.status").value("QUEUED"));
}
@Test
@WithMockUser(authorities = "READ_ALL")
void getDocumentOcrStatus_returnsNone_whenNoOcrJobExists() throws Exception {