From c59287fcfc8049345aab47144d0df8a919105c6d Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 25 Apr 2026 19:18:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(bulk-edit):=20cycle-3=20polish=20=E2=80=94?= =?UTF-8?q?=20Felix=20C2/C3/C4/C5=20+=20Sara=20coverage=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Felix C2 — `BatchMetadataRequest` controller now uses `@Valid` so future @Size/etc. annotations on the record actually fire. Felix C3 — Auto-clear `$effect` in `+layout.svelte` reads `bulkSelectionStore.size` inside `untrack()` so the effect only re-fires on route change, not on every checkbox toggle. Felix C4 — `BulkDocumentEditLayout` edit-mode hydration loop now lives inside `onMount` (not at top-level script) so the SvelteMap mutation is unambiguously tied to instance lifecycle, matching the pattern used by `WhoWhenSection`/`DescriptionSection` after the cycle-2 fix. Felix C5 — Replaced fully-qualified `java.util.LinkedHashSet` in `DocumentController` with a top-of-file import. Sara coverage — six new spec files / blocks pin the cycle-1 and cycle-2 behaviours that were previously untested: - `WhoWhenSection.svelte.spec.ts` — onMount seeding from initialDateIso / initialLocation; doesn't stomp parent-bound dateIso; hideDate / editMode branch - `DescriptionSection.svelte.spec.ts` — onMount seeding from initialTitle / initialDocumentLocation; doesn't stomp parent-bound values; archive-box / archive-folder fields visible only in editMode - `BulkSelectionBar.svelte.spec.ts` — Esc-scope guard tests for `` open and `aria-expanded` popover present - `BulkDocumentEditLayout.svelte.spec.ts` — topbar reads "Massenbearbeitung" + "werden bearbeitet" in edit mode (not the upload-flavoured "hochladen"/"werden erstellt" copy) - `DocumentControllerTest.patchBulk_returns400_whenArchiveBoxExceeds255Chars` — pins the @Size validator on archiveBox via the @Valid wiring Refs #225, PR #331 Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 5 +- .../controller/DocumentControllerTest.java | 18 +++++++ .../document/BulkDocumentEditLayout.svelte | 11 ++-- .../BulkDocumentEditLayout.svelte.spec.ts | 18 +++++++ .../document/BulkSelectionBar.svelte.spec.ts | 33 ++++++++++++ .../DescriptionSection.svelte.spec.ts | 50 +++++++++++++++++++ .../document/WhoWhenSection.svelte.spec.ts | 42 ++++++++++++++++ frontend/src/routes/+layout.svelte | 11 ++-- 8 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 frontend/src/lib/components/document/DescriptionSection.svelte.spec.ts create mode 100644 frontend/src/lib/components/document/WhoWhenSection.svelte.spec.ts diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index a7867c92..20717feb 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -3,6 +3,7 @@ package org.raddatz.familienarchiv.controller; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -272,7 +273,7 @@ public class DocumentController { // Dedupe duplicate document IDs while preserving submission order. A // double-click on "Alle X editieren" would otherwise hit each document // twice and inflate the `updated` count returned to the user. - java.util.LinkedHashSet uniqueIds = new java.util.LinkedHashSet<>(dto.getDocumentIds()); + LinkedHashSet uniqueIds = new LinkedHashSet<>(dto.getDocumentIds()); for (UUID id : uniqueIds) { try { @@ -324,7 +325,7 @@ public class DocumentController { @PostMapping(value = "/batch-metadata", consumes = MediaType.APPLICATION_JSON_VALUE) @RequirePermission(Permission.READ_ALL) - public List batchMetadata(@RequestBody BatchMetadataRequest request, Authentication authentication) { + public List batchMetadata(@RequestBody @Valid BatchMetadataRequest request, Authentication authentication) { if (request == null || request.ids() == null || request.ids().isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ids is required"); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index 5d2ea90b..cbf2ece6 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -996,6 +996,24 @@ class DocumentControllerTest { .andExpect(jsonPath("$.code").value("BULK_EDIT_TOO_MANY_IDS")); } + @Test + @WithMockUser(authorities = "WRITE_ALL") + void patchBulk_returns400_whenArchiveBoxExceeds255Chars() throws Exception { + // Tobias C2 — DocumentBulkEditDTO.archiveBox carries @Size(max=255). + // Without @Valid on @RequestBody this would silently land an + // arbitrarily long string; the test pins both the annotation and + // the controller-level @Valid wiring. + when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); + UUID id = UUID.randomUUID(); + String tooLong = "x".repeat(256); + + String body = "{\"documentIds\":[\"" + id + "\"],\"archiveBox\":\"" + tooLong + "\"}"; + mockMvc.perform(patch("/api/documents/bulk") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void patchBulk_acceptsExactly500Ids_atTheCap() throws Exception { diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte index 3e062058..12ef0aa3 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte @@ -1,7 +1,7 @@