feat(backend): add AdminController endpoints for thumbnail backfill

- 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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-22 22:05:47 +02:00
parent 09fc871756
commit 323ec1ec54
2 changed files with 71 additions and 0 deletions

View File

@@ -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<MassImportService.ImportStatus> triggerMassImport() {
@@ -47,4 +49,15 @@ public class AdminController {
int count = documentService.backfillFileHashes();
return ResponseEntity.ok(new BackfillResult(count));
}
@PostMapping("/generate-thumbnails")
public ResponseEntity<ThumbnailBackfillService.BackfillStatus> generateThumbnails() {
thumbnailBackfillService.runBackfillAsync();
return ResponseEntity.accepted().body(thumbnailBackfillService.getStatus());
}
@GetMapping("/thumbnail-status")
public ResponseEntity<ThumbnailBackfillService.BackfillStatus> thumbnailStatus() {
return ResponseEntity.ok(thumbnailBackfillService.getStatus());
}
}

View File

@@ -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));
}
}