feat(annotations): add PATCH endpoint for annotation resize/move
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 lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.raddatz.familienarchiv.dto.CreateAnnotationDTO;
|
import org.raddatz.familienarchiv.dto.CreateAnnotationDTO;
|
||||||
|
import org.raddatz.familienarchiv.dto.UpdateAnnotationDTO;
|
||||||
import org.raddatz.familienarchiv.model.AppUser;
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.model.DocumentAnnotation;
|
import org.raddatz.familienarchiv.model.DocumentAnnotation;
|
||||||
@@ -11,6 +12,7 @@ import org.raddatz.familienarchiv.security.RequirePermission;
|
|||||||
import org.raddatz.familienarchiv.service.AnnotationService;
|
import org.raddatz.familienarchiv.service.AnnotationService;
|
||||||
import org.raddatz.familienarchiv.service.DocumentService;
|
import org.raddatz.familienarchiv.service.DocumentService;
|
||||||
import org.raddatz.familienarchiv.service.UserService;
|
import org.raddatz.familienarchiv.service.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -45,6 +47,15 @@ public class AnnotationController {
|
|||||||
return annotationService.createAnnotation(documentId, dto, userId, doc.getFileHash());
|
return annotationService.createAnnotation(documentId, dto, userId, doc.getFileHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{annotationId}")
|
||||||
|
@RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL})
|
||||||
|
public DocumentAnnotation updateAnnotation(
|
||||||
|
@PathVariable UUID documentId,
|
||||||
|
@PathVariable UUID annotationId,
|
||||||
|
@Valid @RequestBody UpdateAnnotationDTO dto) {
|
||||||
|
return annotationService.updateAnnotation(documentId, annotationId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{annotationId}")
|
@DeleteMapping("/{annotationId}")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
@RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL})
|
@RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
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.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
@@ -160,6 +161,91 @@ class AnnotationControllerTest {
|
|||||||
.andExpect(status().isNoContent());
|
.andExpect(status().isNoContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── PATCH /api/documents/{documentId}/annotations/{annotationId} ─────────
|
||||||
|
|
||||||
|
private static final String PATCH_JSON = "{\"x\":0.2,\"y\":0.3}";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patchAnnotation_returns401_whenUnauthenticated() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(PATCH_JSON))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void patchAnnotation_returns403_withoutPermission() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(PATCH_JSON))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchAnnotation_returns200_withWriteAllPermission() throws Exception {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID annotId = UUID.randomUUID();
|
||||||
|
DocumentAnnotation updated = DocumentAnnotation.builder()
|
||||||
|
.id(annotId).documentId(docId).pageNumber(1)
|
||||||
|
.x(0.2).y(0.3).width(0.2).height(0.2).color("#ff0000").build();
|
||||||
|
when(annotationService.updateAnnotation(any(), any(), any())).thenReturn(updated);
|
||||||
|
|
||||||
|
mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(PATCH_JSON))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.x").value(0.2))
|
||||||
|
.andExpect(jsonPath("$.y").value(0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
|
void patchAnnotation_returns200_withAnnotateAllPermission() throws Exception {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID annotId = UUID.randomUUID();
|
||||||
|
DocumentAnnotation updated = DocumentAnnotation.builder()
|
||||||
|
.id(annotId).documentId(docId).pageNumber(1)
|
||||||
|
.x(0.2).y(0.3).width(0.2).height(0.2).color("#ff0000").build();
|
||||||
|
when(annotationService.updateAnnotation(any(), any(), any())).thenReturn(updated);
|
||||||
|
|
||||||
|
mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(PATCH_JSON))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchAnnotation_returns404_whenAnnotationBelongsToDifferentDocument() throws Exception {
|
||||||
|
when(annotationService.updateAnnotation(any(), any(), any()))
|
||||||
|
.thenThrow(DomainException.notFound(ErrorCode.ANNOTATION_NOT_FOUND, "not found"));
|
||||||
|
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(PATCH_JSON))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchAnnotation_returns400_withOutOfBoundsCoordinates() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"x\":-0.1,\"y\":0.3}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
|
void patchAnnotation_returns400_withWidthBelowMinimum() throws Exception {
|
||||||
|
mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"width\":0.005}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
// ─── resolveUserId — unauthenticated / null user / exception branches ─────
|
// ─── resolveUserId — unauthenticated / null user / exception branches ─────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user