diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/OcrController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/OcrController.java index 19f44ab5..a6a65a7b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/OcrController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/OcrController.java @@ -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()); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/TriggerSenderTrainingDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/TriggerSenderTrainingDTO.java index baa8d0f3..bae2fcb4 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/TriggerSenderTrainingDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/TriggerSenderTrainingDTO.java @@ -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 +) {} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/OcrControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/OcrControllerTest.java index 236c3d4a..91757d5e 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/OcrControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/OcrControllerTest.java @@ -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 {