diff --git a/backend/pom.xml b/backend/pom.xml
index f2dc9a8b..da863de2 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -193,8 +193,7 @@
verify
report
-
+
check
verify
@@ -207,7 +206,7 @@
BRANCH
COVEREDRATIO
- 0.42
+ 0.88
diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/AnnotationControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/AnnotationControllerTest.java
index f316bb31..ac353a97 100644
--- a/backend/src/test/java/org/raddatz/familienarchiv/controller/AnnotationControllerTest.java
+++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/AnnotationControllerTest.java
@@ -155,4 +155,51 @@ class AnnotationControllerTest {
mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()))
.andExpect(status().isNoContent());
}
+
+ // ─── resolveUserId — unauthenticated / null user / exception branches ─────
+
+ @Test
+ void createAnnotation_returns401_whenUnauthenticated_resolveUserIdReturnsNull() throws Exception {
+ // authentication == null → resolveUserId returns null
+ mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(ANNOTATION_JSON))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithMockUser(authorities = "ANNOTATE_ALL")
+ void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
+ // findByUsername throws → catch block → resolveUserId returns null
+ UUID docId = UUID.randomUUID();
+ when(userService.findByUsername(any())).thenThrow(new RuntimeException("DB error"));
+ DocumentAnnotation saved = DocumentAnnotation.builder()
+ .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
+ .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
+ when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
+ when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);
+
+ mockMvc.perform(post("/api/documents/" + docId + "/annotations")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(ANNOTATION_JSON))
+ .andExpect(status().isCreated());
+ }
+
+ @Test
+ @WithMockUser(authorities = "ANNOTATE_ALL")
+ void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
+ // findByUsername returns null → user != null = false → resolveUserId returns null
+ UUID docId = UUID.randomUUID();
+ when(userService.findByUsername(any())).thenReturn(null);
+ DocumentAnnotation saved = DocumentAnnotation.builder()
+ .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
+ .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
+ when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
+ when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);
+
+ mockMvc.perform(post("/api/documents/" + docId + "/annotations")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(ANNOTATION_JSON))
+ .andExpect(status().isCreated());
+ }
}
diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java
index 40f01a39..d9c2f31d 100644
--- a/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java
+++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java
@@ -263,4 +263,20 @@ class CommentControllerTest {
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
.andExpect(status().isCreated());
}
+
+ // ─── resolveUser — exception branch ──────────────────────────────────────
+
+ @Test
+ @WithMockUser(authorities = "WRITE_ALL")
+ void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
+ // findByUsername throws → catch block in resolveUser → author null, saves anyway
+ when(userService.findByUsername(any())).thenThrow(new RuntimeException("DB error"));
+ DocumentComment saved = DocumentComment.builder()
+ .id(UUID.randomUUID()).documentId(DOC_ID).content("Test comment").build();
+ when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved);
+
+ mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments")
+ .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
+ .andExpect(status().isCreated());
+ }
}
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 749abd88..d200c6f9 100644
--- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java
+++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java
@@ -213,6 +213,80 @@ class DocumentControllerTest {
.andExpect(jsonPath("$.errors[0].code").value("UNSUPPORTED_FILE_TYPE"));
}
+ // ─── GET /api/documents/{id}/file ────────────────────────────────────────
+
+ @Test
+ @WithMockUser
+ void getDocumentFile_returns404_whenDocHasNoFilePath() throws Exception {
+ UUID id = UUID.randomUUID();
+ Document doc = Document.builder().id(id).title("Brief").build(); // filePath == null
+ when(documentService.getDocumentById(id)).thenReturn(doc);
+
+ mockMvc.perform(get("/api/documents/" + id + "/file"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @WithMockUser
+ void getDocumentFile_returns200_withContentTypeFromDoc() throws Exception {
+ UUID id = UUID.randomUUID();
+ Document doc = Document.builder().id(id).title("Brief")
+ .filePath("docs/brief.pdf").contentType("application/pdf")
+ .originalFilename("brief.pdf").build();
+ when(documentService.getDocumentById(id)).thenReturn(doc);
+ java.io.InputStream stream = new java.io.ByteArrayInputStream(new byte[]{1, 2, 3});
+ when(fileService.downloadFile("docs/brief.pdf"))
+ .thenReturn(new FileService.S3FileDownload(
+ new org.springframework.core.io.InputStreamResource(stream), "application/octet-stream"));
+
+ mockMvc.perform(get("/api/documents/" + id + "/file"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithMockUser
+ void getDocumentFile_returns200_withContentTypeFromStorage_whenDocContentTypeIsBlank() throws Exception {
+ UUID id = UUID.randomUUID();
+ Document doc = Document.builder().id(id).title("Brief")
+ .filePath("docs/brief.pdf").contentType(" ") // blank → falls back to storage type
+ .originalFilename("brief.pdf").build();
+ when(documentService.getDocumentById(id)).thenReturn(doc);
+ java.io.InputStream stream = new java.io.ByteArrayInputStream(new byte[]{1, 2, 3});
+ when(fileService.downloadFile("docs/brief.pdf"))
+ .thenReturn(new FileService.S3FileDownload(
+ new org.springframework.core.io.InputStreamResource(stream), "application/pdf"));
+
+ mockMvc.perform(get("/api/documents/" + id + "/file"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithMockUser
+ void getDocumentFile_returns404_whenStorageFileNotFound() throws Exception {
+ UUID id = UUID.randomUUID();
+ Document doc = Document.builder().id(id).title("Brief")
+ .filePath("docs/missing.pdf").contentType("application/pdf")
+ .originalFilename("missing.pdf").build();
+ when(documentService.getDocumentById(id)).thenReturn(doc);
+ when(fileService.downloadFile("docs/missing.pdf"))
+ .thenThrow(new FileService.StorageFileNotFoundException("not found"));
+
+ mockMvc.perform(get("/api/documents/" + id + "/file"))
+ .andExpect(status().isNotFound());
+ }
+
+ // ─── POST /api/documents/quick-upload — null/empty files ─────────────────
+
+ @Test
+ @WithMockUser(authorities = "WRITE_ALL")
+ void quickUpload_returnsEmptyResult_whenNoFilesPartProvided() throws Exception {
+ mockMvc.perform(multipart("/api/documents/quick-upload"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.created").isEmpty())
+ .andExpect(jsonPath("$.updated").isEmpty())
+ .andExpect(jsonPath("$.errors").isEmpty());
+ }
+
// ─── GET /api/documents/incomplete-count ─────────────────────────────────
@Test
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 e04ba664..a56df834 100644
--- a/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java
+++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java
@@ -291,4 +291,17 @@ class PersonControllerTest {
.content("{\"targetPersonId\":\"" + targetId + "\"}"))
.andExpect(status().isNoContent());
}
+
+ // ─── PUT /api/persons/{id} — lastName blank branch ────────────────────────
+
+ @Test
+ @WithMockUser(authorities = "WRITE_ALL")
+ void updatePerson_returns400_whenLastNameIsBlank() throws Exception {
+ // firstName valid, lastName blank → second || operand = true → 400
+ UUID id = UUID.randomUUID();
+ mockMvc.perform(put("/api/persons/{id}", id)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"firstName\":\"Hans\",\"lastName\":\" \"}"))
+ .andExpect(status().isBadRequest());
+ }
}
diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/UserControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/UserControllerTest.java
new file mode 100644
index 00000000..eb4cc3e7
--- /dev/null
+++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/UserControllerTest.java
@@ -0,0 +1,53 @@
+package org.raddatz.familienarchiv.controller;
+
+import org.junit.jupiter.api.Test;
+import org.raddatz.familienarchiv.config.SecurityConfig;
+import org.raddatz.familienarchiv.model.AppUser;
+import org.raddatz.familienarchiv.security.PermissionAspect;
+import org.raddatz.familienarchiv.service.CustomUserDetailsService;
+import org.raddatz.familienarchiv.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(UserController.class)
+@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
+class UserControllerTest {
+
+ @Autowired MockMvc mockMvc;
+
+ @MockitoBean UserService userService;
+ @MockitoBean CustomUserDetailsService customUserDetailsService;
+
+ // ─── GET /api/users/me ────────────────────────────────────────────────────
+
+ @Test
+ void getCurrentUser_returns401_whenUnauthenticated() throws Exception {
+ // authentication == null → returns 401 (covers null/!isAuthenticated branch)
+ mockMvc.perform(get("/api/users/me"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithMockUser(username = "anna")
+ void getCurrentUser_returns200_whenAuthenticated() throws Exception {
+ AppUser user = AppUser.builder().id(UUID.randomUUID()).username("anna").build();
+ when(userService.findByUsername("anna")).thenReturn(user);
+
+ mockMvc.perform(get("/api/users/me"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.username").value("anna"));
+ }
+}