feat(#248): add POST /api/tags/{id}/merge and DELETE /api/tags/{id}/subtree endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package org.raddatz.familienarchiv.controller;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.raddatz.familienarchiv.dto.MergeTagDTO;
|
||||
import org.raddatz.familienarchiv.dto.TagTreeNodeDTO;
|
||||
import org.raddatz.familienarchiv.dto.TagUpdateDTO;
|
||||
import org.raddatz.familienarchiv.model.Tag;
|
||||
@@ -10,15 +11,19 @@ import org.raddatz.familienarchiv.security.Permission;
|
||||
import org.raddatz.familienarchiv.security.RequirePermission;
|
||||
import org.raddatz.familienarchiv.service.DocumentService;
|
||||
import org.raddatz.familienarchiv.service.TagService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -52,4 +57,19 @@ public class TagController {
|
||||
public List<TagTreeNodeDTO> getTagTree() {
|
||||
return tagService.getTagTree();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/merge")
|
||||
@RequirePermission(Permission.ADMIN_TAG)
|
||||
public ResponseEntity<Tag> mergeTag(@PathVariable UUID id, @RequestBody MergeTagDTO dto) {
|
||||
if (dto.targetId() == null)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "targetId required");
|
||||
return ResponseEntity.ok(tagService.mergeTags(id, dto.targetId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/subtree")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@RequirePermission(Permission.ADMIN_TAG)
|
||||
public void deleteSubtree(@PathVariable UUID id) {
|
||||
tagService.deleteWithDescendants(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.raddatz.familienarchiv.dto.MergeTagDTO;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
@@ -107,6 +112,83 @@ class TagControllerTest {
|
||||
.andExpect(jsonPath("$[0].children[0].name").value("Haus"));
|
||||
}
|
||||
|
||||
// ─── POST /api/tags/{id}/merge ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void mergeTag_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetId\": \"" + UUID.randomUUID() + "\"}"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void mergeTag_returns403_whenMissingAdminTagPermission() throws Exception {
|
||||
mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetId\": \"" + UUID.randomUUID() + "\"}"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN_TAG")
|
||||
void mergeTag_returns400_whenTargetIdIsNull() throws Exception {
|
||||
mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN_TAG")
|
||||
void mergeTag_returns404_whenSourceTagNotFound() throws Exception {
|
||||
when(tagService.mergeTags(any(), any()))
|
||||
.thenThrow(DomainException.notFound(ErrorCode.TAG_NOT_FOUND, "Tag not found"));
|
||||
|
||||
mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetId\": \"" + UUID.randomUUID() + "\"}"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN_TAG")
|
||||
void mergeTag_returns200_withTargetTag_onSuccess() throws Exception {
|
||||
UUID targetId = UUID.randomUUID();
|
||||
Tag target = Tag.builder().id(targetId).name("Target").build();
|
||||
when(tagService.mergeTags(any(), any())).thenReturn(target);
|
||||
|
||||
mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetId\": \"" + targetId + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(targetId.toString()))
|
||||
.andExpect(jsonPath("$.name").value("Target"));
|
||||
}
|
||||
|
||||
// ─── DELETE /api/tags/{id}/subtree ────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void deleteSubtree_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void deleteSubtree_returns403_whenMissingAdminTagPermission() throws Exception {
|
||||
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN_TAG")
|
||||
void deleteSubtree_returns204_whenHasAdminTagPermission() throws Exception {
|
||||
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree"))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
// ─── DELETE /api/tags/{id} ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user