From 323ec1ec547f6c513ed04b4229b5399d2469566f Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 22:05:47 +0200 Subject: [PATCH] feat(backend): add AdminController endpoints for thumbnail backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/admin/generate-thumbnails → triggers async backfill, 202 - GET /api/admin/thumbnail-status → returns current BackfillStatus Both gated by the class-level @RequirePermission(Permission.ADMIN). Shape and polling semantics mirror the mass-import endpoints. Refs #307 Co-Authored-By: Claude Opus 4.7 --- .../controller/AdminController.java | 13 +++++ .../controller/AdminControllerTest.java | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/AdminController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/AdminController.java index 918697cc..4311b87a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/AdminController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/AdminController.java @@ -6,6 +6,7 @@ import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.DocumentVersionService; import org.raddatz.familienarchiv.service.MassImportService; +import org.raddatz.familienarchiv.service.ThumbnailBackfillService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -23,6 +24,7 @@ public class AdminController { private final MassImportService massImportService; private final DocumentService documentService; private final DocumentVersionService documentVersionService; + private final ThumbnailBackfillService thumbnailBackfillService; @PostMapping("/trigger-import") public ResponseEntity triggerMassImport() { @@ -47,4 +49,15 @@ public class AdminController { int count = documentService.backfillFileHashes(); return ResponseEntity.ok(new BackfillResult(count)); } + + @PostMapping("/generate-thumbnails") + public ResponseEntity generateThumbnails() { + thumbnailBackfillService.runBackfillAsync(); + return ResponseEntity.accepted().body(thumbnailBackfillService.getStatus()); + } + + @GetMapping("/thumbnail-status") + public ResponseEntity thumbnailStatus() { + return ResponseEntity.ok(thumbnailBackfillService.getStatus()); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/AdminControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/AdminControllerTest.java index a456183b..6a68083b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/AdminControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/AdminControllerTest.java @@ -8,6 +8,7 @@ import org.raddatz.familienarchiv.service.CustomUserDetailsService; import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.DocumentVersionService; import org.raddatz.familienarchiv.service.MassImportService; +import org.raddatz.familienarchiv.service.ThumbnailBackfillService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; @@ -16,10 +17,13 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDateTime; import java.util.List; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -33,6 +37,7 @@ class AdminControllerTest { @MockitoBean MassImportService massImportService; @MockitoBean DocumentService documentService; @MockitoBean DocumentVersionService documentVersionService; + @MockitoBean ThumbnailBackfillService thumbnailBackfillService; @MockitoBean CustomUserDetailsService customUserDetailsService; @Test @@ -83,4 +88,57 @@ class AdminControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.count").value(3)); } + + // ─── POST /api/admin/generate-thumbnails ─────────────────────────────────── + + @Test + void generateThumbnails_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(post("/api/admin/generate-thumbnails")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(roles = "USER") + void generateThumbnails_returns403_whenNotAdmin() throws Exception { + mockMvc.perform(post("/api/admin/generate-thumbnails")) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "ADMIN") + void generateThumbnails_returns202_withStatus_whenAdmin() throws Exception { + ThumbnailBackfillService.BackfillStatus status = new ThumbnailBackfillService.BackfillStatus( + ThumbnailBackfillService.State.RUNNING, "running…", 10, 0, 0, 0, LocalDateTime.now()); + when(thumbnailBackfillService.getStatus()).thenReturn(status); + + mockMvc.perform(post("/api/admin/generate-thumbnails")) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$.state").value("RUNNING")) + .andExpect(jsonPath("$.total").value(10)); + + verify(thumbnailBackfillService).runBackfillAsync(); + } + + // ─── GET /api/admin/thumbnail-status ─────────────────────────────────────── + + @Test + void thumbnailStatus_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/admin/thumbnail-status")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(authorities = "ADMIN") + void thumbnailStatus_returns200_withCurrentStatus_whenAdmin() throws Exception { + ThumbnailBackfillService.BackfillStatus status = new ThumbnailBackfillService.BackfillStatus( + ThumbnailBackfillService.State.DONE, "Fertig: 5 erzeugt, 0 übersprungen, 0 fehlgeschlagen.", + 5, 5, 0, 0, LocalDateTime.now()); + when(thumbnailBackfillService.getStatus()).thenReturn(status); + + mockMvc.perform(get("/api/admin/thumbnail-status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.state").value("DONE")) + .andExpect(jsonPath("$.processed").value(5)) + .andExpect(jsonPath("$.total").value(5)); + } }