From 70281d2c4b321611c1b7bec621c69543e053eed3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 14 May 2026 11:50:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(import):=20add=20structured=20statusCode?= =?UTF-8?q?=20to=20ImportStatus=20=E2=80=94=20replaces=20raw=20German=20me?= =?UTF-8?q?ssage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a statusCode field (IMPORT_IDLE / IMPORT_RUNNING / IMPORT_DONE / IMPORT_FAILED_NO_SPREADSHEET / IMPORT_FAILED_INTERNAL) to ImportStatus. The frontend will map these codes to localized strings via Paraglide instead of rendering the backend's German message verbatim. NoSpreadsheetException distinguishes a missing spreadsheet from other I/O failures so the frontend can show a specific error without raw text. Co-Authored-By: Claude Sonnet 4.6 --- .../importing/MassImportService.java | 21 +++++++++++++------ .../importing/MassImportServiceTest.java | 19 +++++++++++++++-- .../user/AdminControllerTest.java | 16 ++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java b/backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java index f58233ff..c7a575fc 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java @@ -52,9 +52,9 @@ public class MassImportService { public enum State { IDLE, RUNNING, DONE, FAILED } - public record ImportStatus(State state, String message, int processed, LocalDateTime startedAt) {} + public record ImportStatus(State state, String statusCode, String message, int processed, LocalDateTime startedAt) {} - private volatile ImportStatus currentStatus = new ImportStatus(State.IDLE, "Kein Import gestartet.", 0, null); + private volatile ImportStatus currentStatus = new ImportStatus(State.IDLE, "IMPORT_IDLE", "Kein Import gestartet.", 0, null); public ImportStatus getStatus() { return currentStatus; @@ -116,20 +116,29 @@ public class MassImportService { if (currentStatus.state() == State.RUNNING) { throw DomainException.conflict(ErrorCode.IMPORT_ALREADY_RUNNING, "A mass import is already in progress"); } - currentStatus = new ImportStatus(State.RUNNING, "Import läuft...", 0, LocalDateTime.now()); + currentStatus = new ImportStatus(State.RUNNING, "IMPORT_RUNNING", "Import läuft...", 0, LocalDateTime.now()); try { File spreadsheet = findSpreadsheetFile(); log.info("Starte Massenimport aus: {}", spreadsheet.getAbsolutePath()); int processed = processRows(readSpreadsheet(spreadsheet)); - currentStatus = new ImportStatus(State.DONE, + currentStatus = new ImportStatus(State.DONE, "IMPORT_DONE", "Import abgeschlossen. " + processed + " Dokumente verarbeitet.", processed, currentStatus.startedAt()); + } catch (NoSpreadsheetException e) { + log.error("Massenimport fehlgeschlagen: keine Tabellendatei", e); + currentStatus = new ImportStatus(State.FAILED, "IMPORT_FAILED_NO_SPREADSHEET", + "Fehler: " + e.getMessage(), 0, currentStatus.startedAt()); } catch (Exception e) { log.error("Massenimport fehlgeschlagen", e); - currentStatus = new ImportStatus(State.FAILED, "Fehler: " + e.getMessage(), 0, currentStatus.startedAt()); + currentStatus = new ImportStatus(State.FAILED, "IMPORT_FAILED_INTERNAL", + "Fehler: " + e.getMessage(), 0, currentStatus.startedAt()); } } + private static class NoSpreadsheetException extends RuntimeException { + NoSpreadsheetException(String message) { super(message); } + } + private File findSpreadsheetFile() throws IOException { try (Stream files = Files.list(Paths.get(importDir))) { return files @@ -138,7 +147,7 @@ public class MassImportService { return name.endsWith(".ods") || name.endsWith(".xlsx") || name.endsWith(".xls"); }) .findFirst() - .orElseThrow(() -> new RuntimeException( + .orElseThrow(() -> new NoSpreadsheetException( "Keine Tabellendatei (.ods/.xlsx/.xls) in " + importDir + " gefunden!")) .toFile(); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/importing/MassImportServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/importing/MassImportServiceTest.java index a5fe50c6..a3ab9cf9 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/importing/MassImportServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/importing/MassImportServiceTest.java @@ -70,14 +70,20 @@ class MassImportServiceTest { assertThat(service.getStatus().state()).isEqualTo(MassImportService.State.IDLE); } + @Test + void getStatus_hasStatusCode_IMPORT_IDLE_byDefault() { + assertThat(service.getStatus().statusCode()).isEqualTo("IMPORT_IDLE"); + } + // ─── runImportAsync ─────────────────────────────────────────────────────── @Test void runImportAsync_setsFailedStatus_whenImportDirectoryDoesNotExist() { - // /import directory doesn't exist in test environment → findSpreadsheetFile throws + // /import directory doesn't exist in test environment → IOException → IMPORT_FAILED_INTERNAL service.runImportAsync(); assertThat(service.getStatus().state()).isEqualTo(MassImportService.State.FAILED); + assertThat(service.getStatus().statusCode()).isEqualTo("IMPORT_FAILED_INTERNAL"); } @Test @@ -93,10 +99,19 @@ class MassImportServiceTest { assertThat(service.getStatus().message()).contains(tempDir.toString()); } + @Test + void runImportAsync_setsStatusCode_IMPORT_FAILED_NO_SPREADSHEET_whenDirIsEmpty(@TempDir Path tempDir) { + ReflectionTestUtils.setField(service, "importDir", tempDir.toString()); + + service.runImportAsync(); + + assertThat(service.getStatus().statusCode()).isEqualTo("IMPORT_FAILED_NO_SPREADSHEET"); + } + @Test void runImportAsync_throwsConflict_whenAlreadyRunning() { MassImportService.ImportStatus running = new MassImportService.ImportStatus( - MassImportService.State.RUNNING, "Running...", 0, LocalDateTime.now()); + MassImportService.State.RUNNING, "IMPORT_RUNNING", "Running...", 0, LocalDateTime.now()); ReflectionTestUtils.setField(service, "currentStatus", running); assertThatThrownBy(() -> service.runImportAsync()) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java index 8fc2bf3d..b8437abc 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java @@ -40,6 +40,22 @@ class AdminControllerTest { @MockitoBean ThumbnailBackfillService thumbnailBackfillService; @MockitoBean CustomUserDetailsService customUserDetailsService; + // ─── GET /api/admin/import-status ───────────────────────────────────────── + + @Test + @WithMockUser(authorities = "ADMIN") + void importStatus_returns200_withStatusCode_whenAdmin() throws Exception { + MassImportService.ImportStatus status = new MassImportService.ImportStatus( + MassImportService.State.IDLE, "IMPORT_IDLE", "Kein Import gestartet.", 0, null); + when(massImportService.getStatus()).thenReturn(status); + + mockMvc.perform(get("/api/admin/import-status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.state").value("IDLE")) + .andExpect(jsonPath("$.statusCode").value("IMPORT_IDLE")) + .andExpect(jsonPath("$.processed").value(0)); + } + @Test void backfillVersions_returns401_whenUnauthenticated() throws Exception { mockMvc.perform(post("/api/admin/backfill-versions"))