From a1d63bbc429d2974298053578aae182f8ec848d7 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 7 Apr 2026 13:09:58 +0200 Subject: [PATCH] feat(api): add GET/POST/DELETE /api/persons/{id}/aliases endpoints GET returns aliases (no permission required), POST requires WRITE_ALL, DELETE requires WRITE_ALL. 5 new controller tests. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/PersonController.java | 22 +++++++ .../controller/PersonControllerTest.java | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java index 59921087..78da866a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java @@ -5,10 +5,12 @@ import java.util.Map; import java.util.UUID; +import org.raddatz.familienarchiv.dto.PersonNameAliasDTO; import org.raddatz.familienarchiv.dto.PersonSummaryDTO; import org.raddatz.familienarchiv.dto.PersonUpdateDTO; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.model.PersonNameAlias; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.service.DocumentService; @@ -92,4 +94,24 @@ public class PersonController { } personService.mergePersons(id, UUID.fromString(targetIdStr)); } + + // ─── Alias endpoints ──────────────────────────────────────────────────── + + @GetMapping("/{id}/aliases") + public List getAliases(@PathVariable UUID id) { + return personService.getAliases(id); + } + + @PostMapping("/{id}/aliases") + @RequirePermission(Permission.WRITE_ALL) + public PersonNameAlias addAlias(@PathVariable UUID id, @RequestBody PersonNameAliasDTO dto) { + return personService.addAlias(id, dto); + } + + @DeleteMapping("/{id}/aliases/{aliasId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @RequirePermission(Permission.WRITE_ALL) + public void removeAlias(@PathVariable UUID id, @PathVariable UUID aliasId) { + personService.removeAlias(id, aliasId); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java index bafb209f..cf29b596 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java @@ -3,6 +3,8 @@ package org.raddatz.familienarchiv.controller; import org.junit.jupiter.api.Test; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.model.PersonNameAlias; +import org.raddatz.familienarchiv.model.PersonNameAliasType; import org.raddatz.familienarchiv.security.PermissionAspect; import org.raddatz.familienarchiv.service.CustomUserDetailsService; import org.raddatz.familienarchiv.service.DocumentService; @@ -25,6 +27,7 @@ import org.raddatz.familienarchiv.dto.PersonSummaryDTO; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -393,4 +396,66 @@ class PersonControllerTest { .content("{\"targetPersonId\":\"" + UUID.randomUUID() + "\"}")) .andExpect(status().isForbidden()); } + + // ─── GET /api/persons/{id}/aliases ──────────────────────────────────────── + + @Test + @WithMockUser + void getAliases_returns200_withList() throws Exception { + UUID personId = UUID.randomUUID(); + PersonNameAlias alias = PersonNameAlias.builder() + .id(UUID.randomUUID()).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build(); + when(personService.getAliases(personId)).thenReturn(List.of(alias)); + + mockMvc.perform(get("/api/persons/{id}/aliases", personId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].lastName").value("de Gruyter")); + } + + // ─── POST /api/persons/{id}/aliases ────────────────────────────────────── + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void addAlias_returns200_whenValid() throws Exception { + UUID personId = UUID.randomUUID(); + PersonNameAlias saved = PersonNameAlias.builder() + .id(UUID.randomUUID()).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build(); + when(personService.addAlias(eq(personId), any())).thenReturn(saved); + + mockMvc.perform(post("/api/persons/{id}/aliases", personId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"lastName\":\"de Gruyter\",\"type\":\"BIRTH\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lastName").value("de Gruyter")); + } + + @Test + @WithMockUser(authorities = "READ_ALL") + void addAlias_returns403_withoutWritePermission() throws Exception { + mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"lastName\":\"de Gruyter\",\"type\":\"BIRTH\"}")) + .andExpect(status().isForbidden()); + } + + // ─── DELETE /api/persons/{id}/aliases/{aliasId} ────────────────────────── + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void removeAlias_returns204_whenValid() throws Exception { + UUID personId = UUID.randomUUID(); + UUID aliasId = UUID.randomUUID(); + + mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", personId, aliasId)) + .andExpect(status().isNoContent()); + + verify(personService).removeAlias(personId, aliasId); + } + + @Test + @WithMockUser(authorities = "READ_ALL") + void removeAlias_returns403_withoutWritePermission() throws Exception { + mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", UUID.randomUUID(), UUID.randomUUID())) + .andExpect(status().isForbidden()); + } }