From 9b4da70f52c8d1ae6fe69cd7451a0a8c3b20a78d Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 18 May 2026 12:21:15 +0200 Subject: [PATCH] feat(security): enable CSRF protection with CookieCsrfTokenRepository Re-enables Spring Security's CSRF filter (was disabled with a TODO comment). Uses CookieCsrfTokenRepository so the frontend can read the XSRF-TOKEN cookie and send it as X-XSRF-TOKEN on state-mutating requests. Returns CSRF_TOKEN_MISSING error code on 403 instead of generic FORBIDDEN. Updates all WebMvcTest classes to include .with(csrf()) on POST/PUT/PATCH/ DELETE/multipart requests, and fixes integration tests to supply the XSRF-TOKEN cookie + header directly (lazy generation in Spring Security 7). Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/audit/AuditKind.java | 10 +- .../exception/DomainException.java | 4 + .../familienarchiv/exception/ErrorCode.java | 4 + .../security/SecurityConfig.java | 39 +++++--- .../auth/AuthSessionControllerTest.java | 32 ++++++- .../auth/AuthSessionIntegrationTest.java | 37 ++++++-- .../document/DocumentControllerTest.java | 93 ++++++++++--------- .../annotation/AnnotationControllerTest.java | 45 ++++----- .../comment/CommentControllerTest.java | 29 +++--- .../TranscriptionBlockControllerTest.java | 55 +++++------ .../geschichte/GeschichteControllerTest.java | 15 +-- .../NotificationControllerTest.java | 15 +-- .../familienarchiv/ocr/OcrControllerTest.java | 29 +++--- .../person/PersonControllerTest.java | 61 ++++++------ .../RelationshipControllerTest.java | 13 +-- .../familienarchiv/tag/TagControllerTest.java | 29 +++--- .../user/AdminControllerTest.java | 19 ++-- .../user/AuthControllerTest.java | 11 ++- .../user/InviteControllerTest.java | 15 +-- .../user/UserControllerTest.java | 19 ++-- 20 files changed, 330 insertions(+), 244 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java index 3ceb8f39..62f04874 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java @@ -43,8 +43,14 @@ public enum AuditKind { /** Payload: {@code {"email": "addr", "ip": "1.2.3.4", "ua": "Mozilla/5.0..."}} — password NEVER included */ LOGIN_FAILED, - /** Payload: {@code {"userId": "uuid", "ip": "1.2.3.4", "ua": "Mozilla/5.0..."}} */ - LOGOUT; + /** Payload: {@code {"userId": "uuid", "ip": "1.2.3.4", "ua": "Mozilla/5.0...", "reason": "password_change|password_reset|admin_force_logout", "revokedCount": 3}} */ + LOGOUT, + + /** Payload: {@code {"actorId": "uuid", "targetUserId": "uuid", "revokedCount": 3}} */ + ADMIN_FORCE_LOGOUT, + + /** Payload: {@code {"ip": "1.2.3.4", "email": "addr"}} — password NEVER included */ + LOGIN_RATE_LIMITED; public static final Set ROLLUP_ELIGIBLE = Set.of( TEXT_SAVED, FILE_UPLOADED, ANNOTATION_CREATED, diff --git a/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java b/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java index b911059e..c65ad5b8 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java @@ -55,4 +55,8 @@ public class DomainException extends RuntimeException { public static DomainException internal(ErrorCode code, String message) { return new DomainException(code, HttpStatus.INTERNAL_SERVER_ERROR, message); } + + public static DomainException tooManyRequests(ErrorCode code, String message) { + return new DomainException(code, HttpStatus.TOO_MANY_REQUESTS, message); + } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java b/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java index 7489bb83..54802f86 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java @@ -68,6 +68,10 @@ public enum ErrorCode { SESSION_EXPIRED, /** The password-reset token is missing, expired, or already used. 400 */ INVALID_RESET_TOKEN, + /** CSRF token is missing or does not match the expected value. 403 */ + CSRF_TOKEN_MISSING, + /** The login rate limit has been exceeded for this IP/email combination. 429 */ + TOO_MANY_LOGIN_ATTEMPTS, // --- Annotations --- /** The annotation with the given ID does not exist. 404 */ diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java index 80747a8f..1a5b904f 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java @@ -1,7 +1,9 @@ package org.raddatz.familienarchiv.security; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.user.CustomUserDetailsService; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; @@ -19,6 +21,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfException; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; + +import java.util.Map; @Configuration @EnableWebSecurity @@ -78,15 +85,13 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // CSRF is intentionally disabled. The session model relies on: - // 1. SameSite=Strict on the fa_session cookie — a cross-site POST from - // evil.com cannot include the cookie. - // 2. CORS — Spring's default rejects cross-origin requests with credentials - // unless explicitly allowed (no allowedOrigins config). - // - // If either of those is ever weakened, CSRF protection MUST be re-enabled. - // Re-enabling CSRF (CookieCsrfTokenRepository) is planned for Phase 2 (#524). - .csrf(csrf -> csrf.disable()) + // CSRF protection via CookieCsrfTokenRepository (NFR-SEC-103). + // The backend sets an XSRF-TOKEN cookie (not HttpOnly so JS can read it). + // All state-changing requests must include X-XSRF-TOKEN matching the cookie. + // See ADR-020 and issue #524 for the full security rationale. + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())) .authorizeHttpRequests(auth -> { // Actuator endpoints are governed by managementFilterChain (@Order(1)) above. @@ -112,10 +117,18 @@ public class SecurityConfig { // erlaubt pdf im Iframe .headers(headers -> headers .frameOptions(frameOptions -> frameOptions.sameOrigin())) - // Return 401 (not 302 redirect to /login) for unauthenticated API requests. - // httpBasic and formLogin are removed — authentication is via Spring Session only. - .exceptionHandling(ex -> ex.authenticationEntryPoint( - (req, res, e) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED))); + // Return 401 for unauthenticated requests; 403+CSRF_TOKEN_MISSING for CSRF failures. + .exceptionHandling(ex -> ex + .authenticationEntryPoint( + (req, res, e) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED)) + .accessDeniedHandler((req, res, e) -> { + res.setStatus(HttpServletResponse.SC_FORBIDDEN); + res.setContentType("application/json;charset=UTF-8"); + ErrorCode code = (e instanceof CsrfException) + ? ErrorCode.CSRF_TOKEN_MISSING + : ErrorCode.FORBIDDEN; + res.getWriter().write(new ObjectMapper().writeValueAsString(Map.of("code", code.name()))); + })); return http.build(); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java index 7ace8c45..e7860af0 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionControllerTest.java @@ -23,6 +23,7 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -48,6 +49,7 @@ class AuthSessionControllerTest { .thenReturn(new LoginResult(appUser, auth)); mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"user@test.de\",\"password\":\"pass123\"}")) .andExpect(status().isOk()) @@ -61,6 +63,7 @@ class AuthSessionControllerTest { .thenThrow(DomainException.invalidCredentials()); mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"user@test.de\",\"password\":\"wrong\"}")) .andExpect(status().isUnauthorized()) @@ -77,6 +80,7 @@ class AuthSessionControllerTest { // No WithMockUser — must be reachable without an active session mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"pub@test.de\",\"password\":\"pass\"}")) .andExpect(status().isOk()); @@ -91,6 +95,7 @@ class AuthSessionControllerTest { .thenReturn(new LoginResult(appUser, auth)); mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"fix@test.de\",\"password\":\"pass\"}")) .andExpect(status().isOk()); @@ -116,6 +121,7 @@ class AuthSessionControllerTest { .thenReturn(new LoginResult(appUser, auth)); mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"leak@test.de\",\"password\":\"pass\"}")) .andExpect(status().isOk()) @@ -131,12 +137,24 @@ class AuthSessionControllerTest { .thenThrow(DomainException.invalidCredentials()); mockMvc.perform(post("/api/auth/login") + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"email\":\"user@test.de\",\"password\":\"wrong\"}")) .andExpect(status().isUnauthorized()) .andExpect(header().doesNotExist("Set-Cookie")); } + // ─── CSRF protection ────────────────────────────────────────────────────── + + @Test + void authenticated_post_without_csrf_token_returns_403_CSRF_TOKEN_MISSING() throws Exception { + // Red test: CSRF disabled → returns 204; after re-enabling returns 403. + mockMvc.perform(post("/api/auth/logout") + .with(user("user@test.de"))) // authenticated but no CSRF token + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(ErrorCode.CSRF_TOKEN_MISSING.name())); + } + // ─── POST /api/auth/logout ───────────────────────────────────────────────── @Test @@ -144,15 +162,18 @@ class AuthSessionControllerTest { doNothing().when(authService).logout(anyString(), anyString(), anyString()); mockMvc.perform(post("/api/auth/logout") - .with(user("user@test.de"))) + .with(user("user@test.de")) + .with(csrf())) .andExpect(status().isNoContent()); } @Test - void logout_returns_401_when_not_authenticated() throws Exception { - // No authentication at all — Spring Security must return 401 + void logout_without_session_returns_403() throws Exception { + // CsrfFilter runs before AnonymousAuthenticationFilter. When authentication is null, + // ExceptionTranslationFilter routes CSRF AccessDeniedException to accessDeniedHandler → 403. mockMvc.perform(post("/api/auth/logout")) - .andExpect(status().isUnauthorized()); + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(ErrorCode.CSRF_TOKEN_MISSING.name())); } @Test @@ -163,7 +184,8 @@ class AuthSessionControllerTest { .when(authService).logout(anyString(), anyString(), anyString()); mockMvc.perform(post("/api/auth/logout") - .with(user("ghost@test.de"))) + .with(user("ghost@test.de")) + .with(csrf())) .andExpect(status().isNoContent()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionIntegrationTest.java index 92ff991e..1a8786a4 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/auth/AuthSessionIntegrationTest.java @@ -62,7 +62,8 @@ class AuthSessionIntegrationTest { @Test void login_sets_opaque_fa_session_cookie() { - ResponseEntity response = doLogin(); + String xsrf = fetchXsrfToken(); + ResponseEntity response = doLogin(xsrf); assertThat(response.getStatusCode().value()).isEqualTo(200); String cookie = extractFaSessionCookie(response); @@ -73,7 +74,8 @@ class AuthSessionIntegrationTest { @Test void session_cookie_authenticates_subsequent_request() { - String cookie = extractFaSessionCookie(doLogin()); + String xsrf = fetchXsrfToken(); + String cookie = extractFaSessionCookie(doLogin(xsrf)); ResponseEntity me = http.exchange( baseUrl + "/api/users/me", HttpMethod.GET, @@ -84,16 +86,17 @@ class AuthSessionIntegrationTest { @Test void logout_invalidates_session_and_cookie_returns_401_on_reuse() { - String cookie = extractFaSessionCookie(doLogin()); + String xsrf = fetchXsrfToken(); + String sessionCookie = extractFaSessionCookie(doLogin(xsrf)); ResponseEntity logout = http.postForEntity( baseUrl + "/api/auth/logout", - new HttpEntity<>(cookieHeaders(cookie)), Void.class); + new HttpEntity<>(csrfAndSessionHeaders(sessionCookie, xsrf)), Void.class); assertThat(logout.getStatusCode().value()).isEqualTo(204); ResponseEntity me = http.exchange( baseUrl + "/api/users/me", HttpMethod.GET, - new HttpEntity<>(cookieHeaders(cookie)), String.class); + new HttpEntity<>(cookieHeaders(sessionCookie)), String.class); assertThat(me.getStatusCode().value()).isEqualTo(401); } @@ -101,7 +104,8 @@ class AuthSessionIntegrationTest { @Test void session_expired_by_idle_timeout_returns_401() { - String cookie = extractFaSessionCookie(doLogin()); + String xsrf = fetchXsrfToken(); + String cookie = extractFaSessionCookie(doLogin(xsrf)); // Backdate LAST_ACCESS_TIME by 9 hours so lastAccess + maxInactiveInterval(8h) < now long nineHoursAgoMs = System.currentTimeMillis() - 9L * 3600 * 1000; @@ -117,9 +121,20 @@ class AuthSessionIntegrationTest { // ─── helpers ───────────────────────────────────────────────────────────── - private ResponseEntity doLogin() { + /** + * Generates an XSRF token for use in integration tests. + * CookieCsrfTokenRepository validates that Cookie: XSRF-TOKEN=X matches X-XSRF-TOKEN: X. + * By supplying both with the same value we simulate exactly what a browser does. + */ + private String fetchXsrfToken() { + return java.util.UUID.randomUUID().toString(); + } + + private ResponseEntity doLogin(String xsrfToken) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Cookie", "XSRF-TOKEN=" + xsrfToken); + headers.set("X-XSRF-TOKEN", xsrfToken); String body = "{\"email\":\"" + TEST_EMAIL + "\",\"password\":\"" + TEST_PASSWORD + "\"}"; return http.postForEntity(baseUrl + "/api/auth/login", new HttpEntity<>(body, headers), String.class); @@ -131,6 +146,13 @@ class AuthSessionIntegrationTest { return headers; } + private HttpHeaders csrfAndSessionHeaders(String sessionId, String xsrfToken) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Cookie", "fa_session=" + sessionId + "; XSRF-TOKEN=" + xsrfToken); + headers.set("X-XSRF-TOKEN", xsrfToken); + return headers; + } + private String extractFaSessionCookie(ResponseEntity response) { List setCookieHeader = response.getHeaders().get("Set-Cookie"); if (setCookieHeader == null) return ""; @@ -141,6 +163,7 @@ class AuthSessionIntegrationTest { .orElse(""); } + private RestTemplate noThrowRestTemplate() { RestTemplate template = new RestTemplate(); template.setErrorHandler(new DefaultResponseErrorHandler() { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java index 9d85491a..b9f95677 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java @@ -48,6 +48,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(DocumentController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -214,14 +215,14 @@ class DocumentControllerTest { @Test void createDocument_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(multipart("/api/documents")) + mockMvc.perform(multipart("/api/documents").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void createDocument_returns403_whenMissingWritePermission() throws Exception { - mockMvc.perform(multipart("/api/documents")) + mockMvc.perform(multipart("/api/documents").with(csrf())) .andExpect(status().isForbidden()); } @@ -235,7 +236,7 @@ class DocumentControllerTest { .build(); when(documentService.createDocument(any(), any())).thenReturn(doc); - mockMvc.perform(multipart("/api/documents")) + mockMvc.perform(multipart("/api/documents").with(csrf())) .andExpect(status().isOk()); } @@ -244,7 +245,7 @@ class DocumentControllerTest { @Test void updateDocument_returns401_whenUnauthenticated() throws Exception { mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID()) - .with(req -> { req.setMethod("PUT"); return req; })) + .with(req -> { req.setMethod("PUT"); return req; }).with(csrf())) .andExpect(status().isUnauthorized()); } @@ -252,7 +253,7 @@ class DocumentControllerTest { @WithMockUser void updateDocument_returns403_whenMissingWritePermission() throws Exception { mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID()) - .with(req -> { req.setMethod("PUT"); return req; })) + .with(req -> { req.setMethod("PUT"); return req; }).with(csrf())) .andExpect(status().isForbidden()); } @@ -269,7 +270,7 @@ class DocumentControllerTest { when(documentService.updateDocument(any(), any(), any(), any())).thenReturn(doc); mockMvc.perform(multipart("/api/documents/" + id) - .with(req -> { req.setMethod("PUT"); return req; })) + .with(req -> { req.setMethod("PUT"); return req; }).with(csrf())) .andExpect(status().isOk()); } @@ -278,7 +279,7 @@ class DocumentControllerTest { @Test void deleteDocument_returns401_whenUnauthenticated() throws Exception { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders - .delete("/api/documents/" + UUID.randomUUID())) + .delete("/api/documents/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isUnauthorized()); } @@ -286,7 +287,7 @@ class DocumentControllerTest { @WithMockUser void deleteDocument_returns403_whenMissingWritePermission() throws Exception { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders - .delete("/api/documents/" + UUID.randomUUID())) + .delete("/api/documents/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @@ -295,7 +296,7 @@ class DocumentControllerTest { void deleteDocument_returns204_whenHasWritePermission() throws Exception { UUID id = UUID.randomUUID(); mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders - .delete("/api/documents/" + id)) + .delete("/api/documents/" + id).with(csrf())) .andExpect(status().isNoContent()); } @@ -303,14 +304,14 @@ class DocumentControllerTest { @Test void quickUpload_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(multipart("/api/documents/quick-upload")) + mockMvc.perform(multipart("/api/documents/quick-upload").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void quickUpload_returns403_whenMissingWritePermission() throws Exception { - mockMvc.perform(multipart("/api/documents/quick-upload")) + mockMvc.perform(multipart("/api/documents/quick-upload").with(csrf())) .andExpect(status().isForbidden()); } @@ -326,7 +327,7 @@ class DocumentControllerTest { org.springframework.mock.web.MockMultipartFile file = new org.springframework.mock.web.MockMultipartFile("files", "scan001.pdf", "application/pdf", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/quick-upload").file(file)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created[0].title").value("scan001")) .andExpect(jsonPath("$.updated").isEmpty()) @@ -345,7 +346,7 @@ class DocumentControllerTest { org.springframework.mock.web.MockMultipartFile file = new org.springframework.mock.web.MockMultipartFile("files", "scan001.pdf", "application/pdf", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/quick-upload").file(file)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").isEmpty()) .andExpect(jsonPath("$.updated[0].title").value("Alter Brief")) @@ -360,7 +361,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("files", "report.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/quick-upload").file(file)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").isEmpty()) .andExpect(jsonPath("$.errors[0].filename").value("report.docx")) @@ -490,7 +491,7 @@ class DocumentControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void quickUpload_returnsEmptyResult_whenNoFilesPartProvided() throws Exception { - mockMvc.perform(multipart("/api/documents/quick-upload")) + mockMvc.perform(multipart("/api/documents/quick-upload").with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").isEmpty()) .andExpect(jsonPath("$.updated").isEmpty()) @@ -640,7 +641,7 @@ class DocumentControllerTest { @Test void patchTrainingLabels_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels") + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}")) .andExpect(status().isUnauthorized()); @@ -649,7 +650,7 @@ class DocumentControllerTest { @Test @WithMockUser void patchTrainingLabels_returns403_whenMissingWritePermission() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels") + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}")) .andExpect(status().isForbidden()); @@ -659,7 +660,7 @@ class DocumentControllerTest { @WithMockUser(authorities = "WRITE_ALL") void patchTrainingLabels_returns204_whenAddingLabel() throws Exception { UUID id = UUID.randomUUID(); - mockMvc.perform(patch("/api/documents/" + id + "/training-labels") + mockMvc.perform(patch("/api/documents/" + id + "/training-labels").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"label\":\"KURRENT_RECOGNITION\",\"enrolled\":true}")) .andExpect(status().isNoContent()); @@ -671,7 +672,7 @@ class DocumentControllerTest { @WithMockUser(authorities = "WRITE_ALL") void patchTrainingLabels_returns204_whenRemovingLabel() throws Exception { UUID id = UUID.randomUUID(); - mockMvc.perform(patch("/api/documents/" + id + "/training-labels") + mockMvc.perform(patch("/api/documents/" + id + "/training-labels").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"label\":\"KURRENT_SEGMENTATION\",\"enrolled\":false}")) .andExpect(status().isNoContent()); @@ -682,7 +683,7 @@ class DocumentControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void patchTrainingLabels_returns400_whenUnknownLabel() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels") + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/training-labels").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"label\":\"UNKNOWN_GARBAGE\",\"enrolled\":true}")) .andExpect(status().isBadRequest()); @@ -696,7 +697,7 @@ class DocumentControllerTest { org.springframework.mock.web.MockMultipartFile file = new org.springframework.mock.web.MockMultipartFile("file", "brief.pdf", "application/pdf", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID() + "/file").file(file)) + mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID() + "/file").file(file).with(csrf())) .andExpect(status().isForbidden()); } @@ -713,7 +714,7 @@ class DocumentControllerTest { org.springframework.mock.web.MockMultipartFile file = new org.springframework.mock.web.MockMultipartFile("file", "brief.pdf", "application/pdf", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/" + id + "/file").file(file)) + mockMvc.perform(multipart("/api/documents/" + id + "/file").file(file).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(id.toString())) .andExpect(jsonPath("$.status").value("UPLOADED")); @@ -726,7 +727,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile( "file", "evil.html", "text/html", "".getBytes()); - mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID() + "/file").file(htmlFile)) + mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID() + "/file").file(htmlFile).with(csrf())) .andExpect(status().isBadRequest()); } @@ -743,7 +744,7 @@ class DocumentControllerTest { org.springframework.mock.web.MockMultipartFile file = new org.springframework.mock.web.MockMultipartFile("file", "brief.pdf", "application/pdf", new byte[]{1}); - mockMvc.perform(multipart("/api/documents/" + id + "/file").file(file)) + mockMvc.perform(multipart("/api/documents/" + id + "/file").file(file).with(csrf())) .andExpect(status().isNotFound()); } @@ -800,7 +801,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("metadata", "metadata", "application/json", ("{\"senderId\":\"" + senderId + "\"}").getBytes()); - mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(f3).file(metadata)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(f3).file(metadata).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created.length()").value(3)) .andExpect(jsonPath("$.created[0].sender.id").value(senderId.toString())) @@ -827,7 +828,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("metadata", "metadata", "application/json", ("{\"senderId\":\"" + senderId + "\"}").getBytes()); - mockMvc.perform(multipart("/api/documents/quick-upload").file(file).file(metadata)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(file).file(metadata).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created").isEmpty()) .andExpect(jsonPath("$.updated[0].sender.id").value(senderId.toString())) @@ -859,7 +860,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("metadata", "metadata", "application/json", "{\"titles\":[\"Alpha\",\"Beta\",\"Gamma\"]}".getBytes()); - mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(f3).file(metadata)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(f3).file(metadata).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.created[0].title").value("Alpha")) .andExpect(jsonPath("$.created[1].title").value("Beta")) @@ -883,7 +884,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("metadata", "metadata", "application/json", "{\"titles\":[\"A\",\"B\",\"C\"]}".getBytes()); - mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(metadata)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(f1).file(f2).file(metadata).with(csrf())) .andExpect(status().isBadRequest()); } @@ -904,7 +905,7 @@ class DocumentControllerTest { new org.springframework.mock.web.MockMultipartFile("metadata", "metadata", "application/json", "{\"tagNames\":[\"Briefwechsel\",\"Krieg\"]}".getBytes()); - mockMvc.perform(multipart("/api/documents/quick-upload").file(file).file(metadata)) + mockMvc.perform(multipart("/api/documents/quick-upload").file(file).file(metadata).with(csrf())) .andExpect(status().isOk()); org.assertj.core.api.Assertions.assertThat(captor.getValue().getTagNames()) @@ -926,7 +927,7 @@ class DocumentControllerTest { "files", "f" + i + ".pdf", "application/pdf", new byte[]{1})); } - mockMvc.perform(builder) + mockMvc.perform(builder.with(csrf())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("BATCH_TOO_LARGE")); } @@ -945,7 +946,7 @@ class DocumentControllerTest { @Test void patchBulk_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(UUID.randomUUID().toString()))) .andExpect(status().isUnauthorized()); @@ -954,7 +955,7 @@ class DocumentControllerTest { @Test @WithMockUser void patchBulk_returns403_forReadAllUser() throws Exception { - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(UUID.randomUUID().toString()))) .andExpect(status().isForbidden()); @@ -965,7 +966,7 @@ class DocumentControllerTest { void patchBulk_returns400_whenDocumentIdsIsEmpty() throws Exception { when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"documentIds\":[]}")) .andExpect(status().isBadRequest()); @@ -976,7 +977,7 @@ class DocumentControllerTest { void patchBulk_returns400_whenDocumentIdsIsMissing() throws Exception { when(userService.findByEmail(any())).thenReturn(AppUser.builder().id(UUID.randomUUID()).build()); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isBadRequest()); @@ -990,7 +991,7 @@ class DocumentControllerTest { String[] ids = new String[501]; for (int i = 0; i < 501; i++) ids[i] = UUID.randomUUID().toString(); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(ids))) .andExpect(status().isBadRequest()) @@ -1009,7 +1010,7 @@ class DocumentControllerTest { String tooLong = "x".repeat(256); String body = "{\"documentIds\":[\"" + id + "\"],\"archiveBox\":\"" + tooLong + "\"}"; - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isBadRequest()); @@ -1025,7 +1026,7 @@ class DocumentControllerTest { String[] ids = new String[500]; for (int i = 0; i < 500; i++) ids[i] = UUID.randomUUID().toString(); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(ids))) .andExpect(status().isOk()) @@ -1042,7 +1043,7 @@ class DocumentControllerTest { // Same id sent three times — controller should dedupe and call the // service exactly once, returning updated=1, not 3. - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(id.toString(), id.toString(), id.toString()))) .andExpect(status().isOk()) @@ -1061,7 +1062,7 @@ class DocumentControllerTest { when(documentService.applyBulkEditToDocument(any(), any(), any())) .thenAnswer(inv -> Document.builder().id(inv.getArgument(0)).build()); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(id1.toString(), id2.toString()))) .andExpect(status().isOk()) @@ -1137,7 +1138,7 @@ class DocumentControllerTest { void batchMetadata_returns401_whenUnauthenticated() throws Exception { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") .contentType(MediaType.APPLICATION_JSON) - .content("{\"ids\":[\"" + UUID.randomUUID() + "\"]}")) + .content("{\"ids\":[\"" + UUID.randomUUID() + "\"]}").with(csrf())) .andExpect(status().isUnauthorized()); } @@ -1146,7 +1147,7 @@ class DocumentControllerTest { void batchMetadata_returns403_forUserWithoutReadAll() throws Exception { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") .contentType(MediaType.APPLICATION_JSON) - .content("{\"ids\":[\"" + UUID.randomUUID() + "\"]}")) + .content("{\"ids\":[\"" + UUID.randomUUID() + "\"]}").with(csrf())) .andExpect(status().isForbidden()); } @@ -1155,7 +1156,7 @@ class DocumentControllerTest { void batchMetadata_returns400_whenIdsEmpty() throws Exception { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") .contentType(MediaType.APPLICATION_JSON) - .content("{\"ids\":[]}")) + .content("{\"ids\":[]}").with(csrf())) .andExpect(status().isBadRequest()); } @@ -1172,7 +1173,7 @@ class DocumentControllerTest { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") .contentType(MediaType.APPLICATION_JSON) - .content(sb.toString())) + .content(sb.toString()).with(csrf())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("BULK_EDIT_TOO_MANY_IDS")); } @@ -1187,7 +1188,7 @@ class DocumentControllerTest { mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") .contentType(MediaType.APPLICATION_JSON) - .content("{\"ids\":[\"" + id + "\"]}")) + .content("{\"ids\":[\"" + id + "\"]}").with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").value(id.toString())) .andExpect(jsonPath("$[0].title").value("Brief")) @@ -1208,7 +1209,7 @@ class DocumentControllerTest { org.raddatz.familienarchiv.exception.ErrorCode.DOCUMENT_NOT_FOUND, "evil\r\nFAKE LOG ENTRY: admin logged in")); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(badId.toString()))) .andExpect(status().isOk()) @@ -1232,7 +1233,7 @@ class DocumentControllerTest { .thenThrow(org.raddatz.familienarchiv.exception.DomainException.notFound( org.raddatz.familienarchiv.exception.ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + badId)); - mockMvc.perform(patch("/api/documents/bulk") + mockMvc.perform(patch("/api/documents/bulk").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(bulkBody(okId.toString(), badId.toString()))) .andExpect(status().isOk()) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java index 20cf8a79..2157f6c7 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java @@ -31,6 +31,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(AnnotationController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -67,7 +68,7 @@ class AnnotationControllerTest { @Test void createAnnotation_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations") + mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isUnauthorized()); @@ -76,7 +77,7 @@ class AnnotationControllerTest { @Test @WithMockUser void createAnnotation_returns403_whenMissingAnnotatePermission() throws Exception { - mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations") + mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isForbidden()); @@ -92,7 +93,7 @@ class AnnotationControllerTest { when(documentService.getDocumentById(any())).thenReturn(Document.builder().build()); when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + docId + "/annotations") + mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isCreated()); @@ -101,7 +102,7 @@ class AnnotationControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void deleteAnnotation_returns204_whenHasWriteAllPermission() throws Exception { - mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isNoContent()); } @@ -115,7 +116,7 @@ class AnnotationControllerTest { when(documentService.getDocumentById(any())).thenReturn(Document.builder().build()); when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + docId + "/annotations") + mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isCreated()) @@ -133,7 +134,7 @@ class AnnotationControllerTest { when(documentService.getDocumentById(any())).thenReturn(Document.builder().build()); when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + docId + "/annotations") + mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isCreated()); @@ -143,28 +144,28 @@ class AnnotationControllerTest { @Test void deleteAnnotation_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void deleteAnnotation_returns403_whenMissingAnnotatePermission() throws Exception { - mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "READ_ALL") void deleteAnnotation_returns403_whenUserHasOnlyReadAllPermission() throws Exception { - mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "ANNOTATE_ALL") void deleteAnnotation_returns204_whenHasAnnotatePermission() throws Exception { - mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isNoContent()); } @@ -174,7 +175,7 @@ class AnnotationControllerTest { @Test void patchAnnotation_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(PATCH_JSON)) .andExpect(status().isUnauthorized()); @@ -183,7 +184,7 @@ class AnnotationControllerTest { @Test @WithMockUser void patchAnnotation_returns403_withoutPermission() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(PATCH_JSON)) .andExpect(status().isForbidden()); @@ -199,7 +200,7 @@ class AnnotationControllerTest { .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) + mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(PATCH_JSON)) .andExpect(status().isOk()) @@ -217,7 +218,7 @@ class AnnotationControllerTest { .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) + mockMvc.perform(patch("/api/documents/" + docId + "/annotations/" + annotId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(PATCH_JSON)) .andExpect(status().isOk()); @@ -229,7 +230,7 @@ class AnnotationControllerTest { 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()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(PATCH_JSON)) .andExpect(status().isNotFound()); @@ -238,7 +239,7 @@ class AnnotationControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void patchAnnotation_returns400_withOutOfBoundsCoordinates() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"x\":-0.1,\"y\":0.3}")) .andExpect(status().isBadRequest()); @@ -247,7 +248,7 @@ class AnnotationControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void patchAnnotation_returns400_withWidthBelowMinimum() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"width\":0.005}")) .andExpect(status().isBadRequest()); @@ -256,7 +257,7 @@ class AnnotationControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void patchAnnotation_returns400_withHeightBelowMinimum() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"height\":0.005}")) .andExpect(status().isBadRequest()); @@ -265,7 +266,7 @@ class AnnotationControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void patchAnnotation_returns400_withXAboveMaximum() throws Exception { - mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()) + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"x\":1.1}")) .andExpect(status().isBadRequest()); @@ -276,7 +277,7 @@ class AnnotationControllerTest { @Test void createAnnotation_returns401_whenUnauthenticated_resolveUserIdReturnsNull() throws Exception { // authentication == null → resolveUserId returns null - mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations") + mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isUnauthorized()); @@ -294,7 +295,7 @@ class AnnotationControllerTest { when(documentService.getDocumentById(any())).thenReturn(Document.builder().build()); when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + docId + "/annotations") + mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isCreated()); @@ -312,7 +313,7 @@ class AnnotationControllerTest { when(documentService.getDocumentById(any())).thenReturn(Document.builder().build()); when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + docId + "/annotations") + mockMvc.perform(post("/api/documents/" + docId + "/annotations").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(ANNOTATION_JSON)) .andExpect(status().isCreated()); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentControllerTest.java index e4630ae7..473b1a7a 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentControllerTest.java @@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(CommentController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -70,7 +71,7 @@ class CommentControllerTest { .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).content("Nice").build(); when(commentService.postBlockComment(any(), any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.blockId").value(blockId.toString())); @@ -79,7 +80,7 @@ class CommentControllerTest { @Test void postBlockComment_returns401_whenUnauthenticated() throws Exception { UUID blockId = UUID.randomUUID(); - mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isUnauthorized()); } @@ -88,7 +89,7 @@ class CommentControllerTest { @WithMockUser void postBlockComment_returns403_whenMissingPermission() throws Exception { UUID blockId = UUID.randomUUID(); - mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isForbidden()); } @@ -101,7 +102,7 @@ class CommentControllerTest { .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).content("Nice").build(); when(commentService.postBlockComment(any(), any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()); } @@ -116,7 +117,7 @@ class CommentControllerTest { .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).content("Test comment").build(); when(commentService.postBlockComment(any(), any(), any(), any(), any())).thenReturn(saved); - mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()); } @@ -127,7 +128,7 @@ class CommentControllerTest { @WithMockUser(authorities = "ANNOTATE_ALL") void replyToBlockComment_returns400_when_blockId_is_not_a_UUID() throws Exception { mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/NOT-A-UUID" - + "/comments/" + COMMENT_ID + "/replies") + + "/comments/" + COMMENT_ID + "/replies").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isBadRequest()); } @@ -136,7 +137,7 @@ class CommentControllerTest { void replyToBlockComment_returns401_whenUnauthenticated() throws Exception { UUID blockId = UUID.randomUUID(); mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId - + "/comments/" + COMMENT_ID + "/replies") + + "/comments/" + COMMENT_ID + "/replies").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isUnauthorized()); } @@ -151,7 +152,7 @@ class CommentControllerTest { when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId - + "/comments/" + COMMENT_ID + "/replies") + + "/comments/" + COMMENT_ID + "/replies").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()); } @@ -166,7 +167,7 @@ class CommentControllerTest { when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId - + "/comments/" + COMMENT_ID + "/replies") + + "/comments/" + COMMENT_ID + "/replies").with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()); } @@ -175,7 +176,7 @@ class CommentControllerTest { @Test void editComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isUnauthorized()); } @@ -187,7 +188,7 @@ class CommentControllerTest { .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isOk()); } @@ -199,7 +200,7 @@ class CommentControllerTest { .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isOk()); } @@ -208,14 +209,14 @@ class CommentControllerTest { @Test void deleteComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) + mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void deleteComment_returns204_whenAuthenticated() throws Exception { - mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) + mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID).with(csrf())) .andExpect(status().isNoContent()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java index f324da69..5cb0769c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(TranscriptionBlockController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -143,7 +144,7 @@ class TranscriptionBlockControllerTest { @Test void createBlock_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(CREATE_JSON)) .andExpect(status().isUnauthorized()); @@ -152,7 +153,7 @@ class TranscriptionBlockControllerTest { @Test @WithMockUser void createBlock_returns403_whenMissingWriteAllPermission() throws Exception { - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(CREATE_JSON)) .andExpect(status().isForbidden()); @@ -164,7 +165,7 @@ class TranscriptionBlockControllerTest { when(userService.findByEmail(any())).thenReturn(mockUser()); when(transcriptionService.createBlock(eq(DOC_ID), any(), any())).thenReturn(sampleBlock()); - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(CREATE_JSON)) .andExpect(status().isCreated()) @@ -177,7 +178,7 @@ class TranscriptionBlockControllerTest { void createBlock_returns401_whenUserNotFoundInDatabase() throws Exception { when(userService.findByEmail(any())).thenReturn(null); - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(CREATE_JSON)) .andExpect(status().isUnauthorized()); @@ -192,7 +193,7 @@ class TranscriptionBlockControllerTest { + "\"mentionedPersons\":[{\"personId\":\"" + UUID.randomUUID() + "\",\"displayName\":\"" + longName + "\"}]}"; - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isBadRequest()) @@ -206,7 +207,7 @@ class TranscriptionBlockControllerTest { String body = "{\"pageNumber\":1,\"x\":0.1,\"y\":0.2,\"width\":0.3,\"height\":0.4,\"text\":\"x\"," + "\"mentionedPersons\":[{\"personId\":null,\"displayName\":\"Auguste Raddatz\"}]}"; - mockMvc.perform(post(URL_BASE) + mockMvc.perform(post(URL_BASE).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isBadRequest()) @@ -217,7 +218,7 @@ class TranscriptionBlockControllerTest { @Test void updateBlock_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_JSON)) .andExpect(status().isUnauthorized()); @@ -226,7 +227,7 @@ class TranscriptionBlockControllerTest { @Test @WithMockUser void updateBlock_returns403_whenMissingWriteAllPermission() throws Exception { - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_JSON)) .andExpect(status().isForbidden()); @@ -243,7 +244,7 @@ class TranscriptionBlockControllerTest { when(transcriptionService.updateBlock(eq(DOC_ID), eq(BLOCK_ID), any(), any())) .thenReturn(updated); - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_JSON)) .andExpect(status().isOk()) @@ -259,7 +260,7 @@ class TranscriptionBlockControllerTest { String body = "{\"text\":\"x\",\"mentionedPersons\":[{\"personId\":\"" + UUID.randomUUID() + "\",\"displayName\":\"" + longName + "\"}]}"; - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isBadRequest()) @@ -272,7 +273,7 @@ class TranscriptionBlockControllerTest { when(userService.findByEmail(any())).thenReturn(mockUser()); String body = "{\"text\":\"x\",\"mentionedPersons\":[{\"personId\":null,\"displayName\":\"Auguste Raddatz\"}]}"; - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isBadRequest()) @@ -286,7 +287,7 @@ class TranscriptionBlockControllerTest { when(transcriptionService.updateBlock(any(), any(), any(), any())) .thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found")); - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_JSON)) .andExpect(status().isNotFound()); @@ -297,7 +298,7 @@ class TranscriptionBlockControllerTest { void updateBlock_returns401_whenUserNotFoundInDatabase() throws Exception { when(userService.findByEmail(any())).thenReturn(null); - mockMvc.perform(put(URL_BLOCK) + mockMvc.perform(put(URL_BLOCK).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_JSON)) .andExpect(status().isUnauthorized()); @@ -307,28 +308,28 @@ class TranscriptionBlockControllerTest { @Test void deleteBlock_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete(URL_BLOCK)) + mockMvc.perform(delete(URL_BLOCK).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void deleteBlock_returns403_whenMissingWriteAllPermission() throws Exception { - mockMvc.perform(delete(URL_BLOCK)) + mockMvc.perform(delete(URL_BLOCK).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "READ_ALL") void deleteBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception { - mockMvc.perform(delete(URL_BLOCK)) + mockMvc.perform(delete(URL_BLOCK).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "WRITE_ALL") void deleteBlock_returns204_whenAuthorised() throws Exception { - mockMvc.perform(delete(URL_BLOCK)) + mockMvc.perform(delete(URL_BLOCK).with(csrf())) .andExpect(status().isNoContent()); } @@ -339,7 +340,7 @@ class TranscriptionBlockControllerTest { DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found")) .when(transcriptionService).deleteBlock(any(), any()); - mockMvc.perform(delete(URL_BLOCK)) + mockMvc.perform(delete(URL_BLOCK).with(csrf())) .andExpect(status().isNotFound()); } @@ -347,7 +348,7 @@ class TranscriptionBlockControllerTest { @Test void reorderBlocks_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put(URL_REORDER) + mockMvc.perform(put(URL_REORDER).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(REORDER_JSON)) .andExpect(status().isUnauthorized()); @@ -356,7 +357,7 @@ class TranscriptionBlockControllerTest { @Test @WithMockUser void reorderBlocks_returns403_whenMissingWriteAllPermission() throws Exception { - mockMvc.perform(put(URL_REORDER) + mockMvc.perform(put(URL_REORDER).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(REORDER_JSON)) .andExpect(status().isForbidden()); @@ -367,7 +368,7 @@ class TranscriptionBlockControllerTest { void reorderBlocks_returns200_withReorderedBlocks_whenAuthorised() throws Exception { when(transcriptionService.listBlocks(DOC_ID)).thenReturn(List.of(sampleBlock())); - mockMvc.perform(put(URL_REORDER) + mockMvc.perform(put(URL_REORDER).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(REORDER_JSON)) .andExpect(status().isOk()) @@ -434,7 +435,7 @@ class TranscriptionBlockControllerTest { when(transcriptionService.reviewBlock(eq(DOC_ID), eq(BLOCK_ID), any())).thenReturn(reviewed); mockMvc.perform(put("/api/documents/{documentId}/transcription-blocks/{blockId}/review", - DOC_ID, BLOCK_ID)) + DOC_ID, BLOCK_ID).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.reviewed").value(true)); } @@ -445,14 +446,14 @@ class TranscriptionBlockControllerTest { @Test void markAllBlocksReviewed_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put(URL_REVIEW_ALL)) + mockMvc.perform(put(URL_REVIEW_ALL).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(authorities = "READ_ALL") void markAllBlocksReviewed_returns403_whenMissingWriteAllPermission() throws Exception { - mockMvc.perform(put(URL_REVIEW_ALL)) + mockMvc.perform(put(URL_REVIEW_ALL).with(csrf())) .andExpect(status().isForbidden()); } @@ -469,7 +470,7 @@ class TranscriptionBlockControllerTest { when(transcriptionService.markAllBlocksReviewed(eq(DOC_ID), any())) .thenReturn(List.of(b1, b2)); - mockMvc.perform(put(URL_REVIEW_ALL)) + mockMvc.perform(put(URL_REVIEW_ALL).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$[0].reviewed").value(true)) @@ -483,7 +484,7 @@ class TranscriptionBlockControllerTest { when(transcriptionService.markAllBlocksReviewed(eq(DOC_ID), any())) .thenReturn(List.of()); - mockMvc.perform(put(URL_REVIEW_ALL)) + mockMvc.perform(put(URL_REVIEW_ALL).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$").isEmpty()); @@ -494,7 +495,7 @@ class TranscriptionBlockControllerTest { void markAllBlocksReviewed_returns401_whenUserNotFoundInDatabase() throws Exception { when(userService.findByEmail(any())).thenReturn(null); - mockMvc.perform(put(URL_REVIEW_ALL)) + mockMvc.perform(put(URL_REVIEW_ALL).with(csrf())) .andExpect(status().isUnauthorized()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteControllerTest.java index d6ce0974..4d13363b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteControllerTest.java @@ -36,6 +36,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(GeschichteController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -130,7 +131,7 @@ class GeschichteControllerTest { @Test void create_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/geschichten") + mockMvc.perform(post("/api/geschichten").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"x\"}")) .andExpect(status().isUnauthorized()); @@ -139,7 +140,7 @@ class GeschichteControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void create_returns403_whenLackingBlogWrite() throws Exception { - mockMvc.perform(post("/api/geschichten") + mockMvc.perform(post("/api/geschichten").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"x\"}")) .andExpect(status().isForbidden()); @@ -155,7 +156,7 @@ class GeschichteControllerTest { GeschichteUpdateDTO dto = new GeschichteUpdateDTO(); dto.setTitle("New"); - mockMvc.perform(post("/api/geschichten") + mockMvc.perform(post("/api/geschichten").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(dto))) .andExpect(status().isCreated()) @@ -167,7 +168,7 @@ class GeschichteControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void update_returns403_whenLackingBlogWrite() throws Exception { - mockMvc.perform(patch("/api/geschichten/{id}", UUID.randomUUID()) + mockMvc.perform(patch("/api/geschichten/{id}", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isForbidden()); @@ -180,7 +181,7 @@ class GeschichteControllerTest { when(geschichteService.update(eq(id), any(GeschichteUpdateDTO.class))) .thenReturn(published(id, "Updated")); - mockMvc.perform(patch("/api/geschichten/{id}", id) + mockMvc.perform(patch("/api/geschichten/{id}", id).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"status\":\"PUBLISHED\"}")) .andExpect(status().isOk()) @@ -192,7 +193,7 @@ class GeschichteControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void delete_returns403_whenLackingBlogWrite() throws Exception { - mockMvc.perform(delete("/api/geschichten/{id}", UUID.randomUUID())) + mockMvc.perform(delete("/api/geschichten/{id}", UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @@ -201,7 +202,7 @@ class GeschichteControllerTest { void delete_returns204_withBlogWrite() throws Exception { UUID id = UUID.randomUUID(); - mockMvc.perform(delete("/api/geschichten/{id}", id)) + mockMvc.perform(delete("/api/geschichten/{id}", id).with(csrf())) .andExpect(status().isNoContent()); verify(geschichteService).delete(id); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/notification/NotificationControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/notification/NotificationControllerTest.java index 451434ff..22654a07 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/notification/NotificationControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/notification/NotificationControllerTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(NotificationController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -141,7 +142,7 @@ class NotificationControllerTest { @Test void markAllRead_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/notifications/read-all")) + mockMvc.perform(post("/api/notifications/read-all").with(csrf())) .andExpect(status().isUnauthorized()); } @@ -151,7 +152,7 @@ class NotificationControllerTest { AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build(); when(userService.findByEmail("testuser")).thenReturn(user); - mockMvc.perform(post("/api/notifications/read-all")) + mockMvc.perform(post("/api/notifications/read-all").with(csrf())) .andExpect(status().isNoContent()); verify(notificationService).markAllRead(USER_ID); @@ -161,7 +162,7 @@ class NotificationControllerTest { @Test void markOneRead_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/notifications/" + UUID.randomUUID() + "/read")) + mockMvc.perform(patch("/api/notifications/" + UUID.randomUUID() + "/read").with(csrf())) .andExpect(status().isUnauthorized()); } @@ -176,7 +177,7 @@ class NotificationControllerTest { org.raddatz.familienarchiv.exception.DomainException.forbidden("not yours")) .when(notificationService).markRead(notifId, USER_ID); - mockMvc.perform(patch("/api/notifications/" + notifId + "/read")) + mockMvc.perform(patch("/api/notifications/" + notifId + "/read").with(csrf())) .andExpect(status().isForbidden()); } @@ -256,7 +257,7 @@ class NotificationControllerTest { .notifyOnReply(true).notifyOnMention(true).build(); when(notificationService.updatePreferences(USER_ID, true, true)).thenReturn(updated); - mockMvc.perform(put("/api/users/me/notification-preferences") + mockMvc.perform(put("/api/users/me/notification-preferences").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"notifyOnReply\":true,\"notifyOnMention\":true}")) .andExpect(status().isOk()) @@ -275,7 +276,7 @@ class NotificationControllerTest { .notifyOnReply(true).notifyOnMention(false).build(); when(notificationService.updatePreferences(USER_ID, true, false)).thenReturn(updated); - mockMvc.perform(put("/api/users/me/notification-preferences") + mockMvc.perform(put("/api/users/me/notification-preferences").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"notifyOnReply\":true,\"notifyOnMention\":false}")) .andExpect(status().isOk()) @@ -337,7 +338,7 @@ class NotificationControllerTest { doThrow(DomainException.notFound(ErrorCode.NOTIFICATION_NOT_FOUND, "Notification not found: " + notifId)) .when(notificationService).markRead(notifId, USER_ID); - mockMvc.perform(patch("/api/notifications/" + notifId + "/read")) + mockMvc.perform(patch("/api/notifications/" + notifId + "/read").with(csrf())) .andExpect(status().isNotFound()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/ocr/OcrControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/ocr/OcrControllerTest.java index 79b5df61..9eb86db0 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/ocr/OcrControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/ocr/OcrControllerTest.java @@ -39,6 +39,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(OcrController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -66,7 +67,7 @@ class OcrControllerTest { when(ocrService.startOcr(eq(docId), eq(ScriptType.TYPEWRITER), any(), anyBoolean())).thenReturn(jobId); - mockMvc.perform(post("/api/documents/{id}/ocr", docId) + mockMvc.perform(post("/api/documents/{id}/ocr", docId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(dto))) .andExpect(status().isAccepted()) @@ -80,7 +81,7 @@ class OcrControllerTest { when(ocrService.startOcr(eq(docId), any(), any(), anyBoolean())) .thenThrow(DomainException.badRequest(ErrorCode.OCR_DOCUMENT_NOT_UPLOADED, "Not uploaded")); - mockMvc.perform(post("/api/documents/{id}/ocr", docId) + mockMvc.perform(post("/api/documents/{id}/ocr", docId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isBadRequest()); @@ -127,7 +128,7 @@ class OcrControllerTest { when(ocrBatchService.startBatch(eq(docIds), any())).thenReturn(jobId); - mockMvc.perform(post("/api/ocr/batch") + mockMvc.perform(post("/api/ocr/batch").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(dto))) .andExpect(status().isAccepted()) @@ -179,14 +180,14 @@ class OcrControllerTest { @Test void triggerTraining_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/ocr/train")) + mockMvc.perform(post("/api/ocr/train").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(authorities = "READ_ALL") void triggerTraining_returns403_whenNotAdmin() throws Exception { - mockMvc.perform(post("/api/ocr/train")) + mockMvc.perform(post("/api/ocr/train").with(csrf())) .andExpect(status().isForbidden()); } @@ -196,7 +197,7 @@ class OcrControllerTest { when(ocrTrainingService.triggerTraining(any())) .thenThrow(DomainException.conflict(ErrorCode.TRAINING_ALREADY_RUNNING, "Already running")); - mockMvc.perform(post("/api/ocr/train")) + mockMvc.perform(post("/api/ocr/train").with(csrf())) .andExpect(status().isConflict()); } @@ -209,7 +210,7 @@ class OcrControllerTest { .blockCount(10).documentCount(3).modelName("german_kurrent").build(); when(ocrTrainingService.triggerTraining(any())).thenReturn(run); - mockMvc.perform(post("/api/ocr/train")) + mockMvc.perform(post("/api/ocr/train").with(csrf())) .andExpect(status().isCreated()) .andExpect(jsonPath("$.status").value("DONE")) .andExpect(jsonPath("$.blockCount").value(10)); @@ -365,7 +366,7 @@ class OcrControllerTest { @Test @WithMockUser(authorities = "ADMIN") void triggerSenderTraining_returns400_whenPersonIdIsNull() throws Exception { - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":null}")) .andExpect(status().isBadRequest()); @@ -373,7 +374,7 @@ class OcrControllerTest { @Test void triggerSenderTraining_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + UUID.randomUUID() + "\"}")) .andExpect(status().isUnauthorized()); @@ -382,7 +383,7 @@ class OcrControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void triggerSenderTraining_returns403_whenNotAdmin() throws Exception { - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + UUID.randomUUID() + "\"}")) .andExpect(status().isForbidden()); @@ -395,7 +396,7 @@ class OcrControllerTest { when(senderModelService.triggerManualSenderTraining(unknownId)) .thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found")); - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + unknownId + "\"}")) .andExpect(status().isNotFound()); @@ -410,7 +411,7 @@ class OcrControllerTest { .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build(); when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run); - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + personId + "\"}")) .andExpect(status().isAccepted()) @@ -426,7 +427,7 @@ class OcrControllerTest { .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build(); when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run); - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + personId + "\"}")) .andExpect(status().isAccepted()) @@ -442,7 +443,7 @@ class OcrControllerTest { .personId(personId).blockCount(5).documentCount(0).modelName("sender_" + personId).build(); when(senderModelService.triggerManualSenderTraining(personId)).thenReturn(run); - mockMvc.perform(post("/api/ocr/train-sender") + mockMvc.perform(post("/api/ocr/train-sender").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"personId\":\"" + personId + "\"}")) .andExpect(status().isAccepted()); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java index c7800da1..e7767411 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java @@ -36,6 +36,7 @@ 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(PersonController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -217,7 +218,7 @@ class PersonControllerTest { @Test void createPerson_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}")) .andExpect(status().isUnauthorized()); @@ -226,7 +227,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void createPerson_returns400_whenPersonTypeIsPerson_andFirstNameIsMissing() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -235,7 +236,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void createPerson_returns400_whenPersonTypeIsPerson_andFirstNameIsBlank() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\" \",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -244,7 +245,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void createPerson_returns400_whenLastNameIsMissing() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -253,7 +254,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void createPerson_returns400_whenLastNameIsBlank() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\" \",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -265,7 +266,7 @@ class PersonControllerTest { Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build(); when(personService.createPerson(any(org.raddatz.familienarchiv.person.PersonUpdateDTO.class))).thenReturn(saved); - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isOk()) @@ -278,7 +279,7 @@ class PersonControllerTest { Person saved = Person.builder().id(UUID.randomUUID()).lastName("Verlag GmbH").build(); when(personService.createPerson(any(org.raddatz.familienarchiv.person.PersonUpdateDTO.class))).thenReturn(saved); - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"Verlag GmbH\",\"personType\":\"INSTITUTION\"}")) .andExpect(status().isOk()) @@ -293,7 +294,7 @@ class PersonControllerTest { Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build(); when(personService.createPerson(captor.capture())).thenReturn(saved); - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"title\":\" Prof. \",\"personType\":\"PERSON\"}")) .andExpect(status().isOk()); @@ -307,7 +308,7 @@ class PersonControllerTest { when(personService.createPerson(any())).thenThrow( DomainException.badRequest(ErrorCode.INVALID_PERSON_TYPE, "SKIP is not a valid person type")); - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"Müller\",\"personType\":\"SKIP\"}")) .andExpect(status().isBadRequest()) @@ -318,7 +319,7 @@ class PersonControllerTest { @Test void updatePerson_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()) + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}")) .andExpect(status().isUnauthorized()); @@ -327,7 +328,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void updatePerson_returns400_whenPersonTypeIsPerson_andFirstNameIsBlank() throws Exception { - mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()) + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -336,7 +337,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void updatePerson_returns400_whenLastNameIsNull() throws Exception { - mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()) + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -349,7 +350,7 @@ class PersonControllerTest { Person updated = Person.builder().id(id).firstName("Hans").lastName("Müller").build(); when(personService.updatePerson(eq(id), any())).thenReturn(updated); - mockMvc.perform(put("/api/persons/{id}", id) + mockMvc.perform(put("/api/persons/{id}", id).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isOk()) @@ -360,7 +361,7 @@ class PersonControllerTest { @Test void mergePerson_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetPersonId\":\"" + UUID.randomUUID() + "\"}")) .andExpect(status().isUnauthorized()); @@ -369,7 +370,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void mergePerson_returns400_whenTargetPersonIdIsMissing() throws Exception { - mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isBadRequest()); @@ -378,7 +379,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void mergePerson_returns400_whenTargetPersonIdIsBlank() throws Exception { - mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetPersonId\":\" \"}")) .andExpect(status().isBadRequest()); @@ -390,7 +391,7 @@ class PersonControllerTest { UUID sourceId = UUID.randomUUID(); UUID targetId = UUID.randomUUID(); - mockMvc.perform(post("/api/persons/{id}/merge", sourceId) + mockMvc.perform(post("/api/persons/{id}/merge", sourceId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetPersonId\":\"" + targetId + "\"}")) .andExpect(status().isNoContent()); @@ -402,7 +403,7 @@ class PersonControllerTest { @WithMockUser(authorities = "WRITE_ALL") void updatePerson_returns400_whenLastNameIsBlank() throws Exception { UUID id = UUID.randomUUID(); - mockMvc.perform(put("/api/persons/{id}", id) + mockMvc.perform(put("/api/persons/{id}", id).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\" \",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -418,7 +419,7 @@ class PersonControllerTest { .alias("Oma Maria").birthYear(1901).deathYear(1975).notes("Some notes").build(); when(personService.createPerson(any(org.raddatz.familienarchiv.person.PersonUpdateDTO.class))).thenReturn(saved); - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Maria\",\"lastName\":\"Raddatz\"," + "\"alias\":\"Oma Maria\",\"birthYear\":1901,\"deathYear\":1975," + @@ -436,7 +437,7 @@ class PersonControllerTest { void updatePerson_returns400_whenNotesExceed5000Chars() throws Exception { String oversizedNotes = "x".repeat(5001); UUID id = UUID.randomUUID(); - mockMvc.perform(put("/api/persons/{id}", id) + mockMvc.perform(put("/api/persons/{id}", id).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"notes\":\"" + oversizedNotes + "\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -447,7 +448,7 @@ class PersonControllerTest { void updatePerson_returns400_whenFirstNameExceeds100Chars() throws Exception { String oversizedFirstName = "x".repeat(101); UUID id = UUID.randomUUID(); - mockMvc.perform(put("/api/persons/{id}", id) + mockMvc.perform(put("/api/persons/{id}", id).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"" + oversizedFirstName + "\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isBadRequest()); @@ -458,7 +459,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void createPerson_returns403_whenUserHasOnlyReadPermission() throws Exception { - mockMvc.perform(post("/api/persons") + mockMvc.perform(post("/api/persons").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isForbidden()); @@ -467,7 +468,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void updatePerson_returns403_whenUserHasOnlyReadPermission() throws Exception { - mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()) + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"}")) .andExpect(status().isForbidden()); @@ -476,7 +477,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void mergePerson_returns403_whenUserHasOnlyReadPermission() throws Exception { - mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetPersonId\":\"" + UUID.randomUUID() + "\"}")) .andExpect(status().isForbidden()); @@ -507,7 +508,7 @@ class PersonControllerTest { .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) + mockMvc.perform(post("/api/persons/{id}/aliases", personId).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"de Gruyter\",\"type\":\"BIRTH\"}")) .andExpect(status().isOk()) @@ -517,7 +518,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void addAlias_returns403_withoutWritePermission() throws Exception { - mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"de Gruyter\",\"type\":\"BIRTH\"}")) .andExpect(status().isForbidden()); @@ -531,7 +532,7 @@ class PersonControllerTest { UUID personId = UUID.randomUUID(); UUID aliasId = UUID.randomUUID(); - mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", personId, aliasId)) + mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", personId, aliasId).with(csrf())) .andExpect(status().isNoContent()); verify(personService).removeAlias(personId, aliasId); @@ -540,14 +541,14 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "READ_ALL") void removeAlias_returns403_withoutWritePermission() throws Exception { - mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", UUID.randomUUID(), UUID.randomUUID())) + mockMvc.perform(delete("/api/persons/{id}/aliases/{aliasId}", UUID.randomUUID(), UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "WRITE_ALL") void addAlias_returns400_whenLastNameIsBlank() throws Exception { - mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"\",\"type\":\"BIRTH\"}")) .andExpect(status().isBadRequest()); @@ -556,7 +557,7 @@ class PersonControllerTest { @Test @WithMockUser(authorities = "WRITE_ALL") void addAlias_returns400_whenTypeIsNull() throws Exception { - mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()) + mockMvc.perform(post("/api/persons/{id}/aliases", UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"lastName\":\"de Gruyter\"}")) .andExpect(status().isBadRequest()); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/relationship/RelationshipControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/relationship/RelationshipControllerTest.java index 5395c8e8..74cc739c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/relationship/RelationshipControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/relationship/RelationshipControllerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(RelationshipController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -67,7 +68,7 @@ class RelationshipControllerTest { @Test @WithMockUser(username = "testuser", authorities = {"READ_ALL"}) void addRelationship_returns403_for_user_with_READ_ALL_only() throws Exception { - mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID) + mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"PARENT_OF\"}")) .andExpect(status().isForbidden()); @@ -76,14 +77,14 @@ class RelationshipControllerTest { @Test @WithMockUser(username = "testuser", authorities = {"READ_ALL"}) void deleteRelationship_returns403_for_READ_ALL_only_user() throws Exception { - mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID())) + mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(username = "testuser", authorities = {"READ_ALL"}) void patchFamilyMember_returns403_for_READ_ALL_only_user() throws Exception { - mockMvc.perform(patch("/api/persons/{id}/family-member", PERSON_ID) + mockMvc.perform(patch("/api/persons/{id}/family-member", PERSON_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"familyMember\":true}")) .andExpect(status().isForbidden()); @@ -125,7 +126,7 @@ class RelationshipControllerTest { @Test @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) void addRelationship_returns400_when_relationType_is_unknown_value() throws Exception { - mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID) + mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"NOT_A_REAL_TYPE\"}")) .andExpect(status().isBadRequest()); @@ -141,7 +142,7 @@ class RelationshipControllerTest { RelationType.PARENT_OF, null, null, null); when(relationshipService.addRelationship(any(), any())).thenReturn(created); - mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID) + mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"PARENT_OF\"}")) .andExpect(status().isCreated()) @@ -154,7 +155,7 @@ class RelationshipControllerTest { UUID relId = UUID.randomUUID(); doNothing().when(relationshipService).deleteRelationship(any(), any()); - mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, relId)) + mockMvc.perform(delete("/api/persons/{id}/relationships/{relId}", PERSON_ID, relId).with(csrf())) .andExpect(status().isNoContent()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/tag/TagControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/tag/TagControllerTest.java index 0d7e387b..1504f1fa 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/tag/TagControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/tag/TagControllerTest.java @@ -29,6 +29,7 @@ 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.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(TagController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -61,7 +62,7 @@ class TagControllerTest { @Test void updateTag_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put("/api/tags/" + UUID.randomUUID()) + mockMvc.perform(put("/api/tags/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\": \"New\"}")) .andExpect(status().isUnauthorized()); @@ -70,7 +71,7 @@ class TagControllerTest { @Test @WithMockUser void updateTag_returns403_whenMissingAdminTagPermission() throws Exception { - mockMvc.perform(put("/api/tags/" + UUID.randomUUID()) + mockMvc.perform(put("/api/tags/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\": \"New\"}")) .andExpect(status().isForbidden()); @@ -82,7 +83,7 @@ class TagControllerTest { Tag tag = Tag.builder().id(UUID.randomUUID()).name("New").build(); when(tagService.update(any(), any())).thenReturn(tag); - mockMvc.perform(put("/api/tags/" + UUID.randomUUID()) + mockMvc.perform(put("/api/tags/" + UUID.randomUUID()).with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\": \"New\"}")) .andExpect(status().isOk()); @@ -116,7 +117,7 @@ class TagControllerTest { @Test void mergeTag_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge") + mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetId\": \"" + UUID.randomUUID() + "\"}")) .andExpect(status().isUnauthorized()); @@ -125,7 +126,7 @@ class TagControllerTest { @Test @WithMockUser void mergeTag_returns403_whenMissingAdminTagPermission() throws Exception { - mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge") + mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetId\": \"" + UUID.randomUUID() + "\"}")) .andExpect(status().isForbidden()); @@ -134,7 +135,7 @@ class TagControllerTest { @Test @WithMockUser(authorities = "ADMIN_TAG") void mergeTag_returns400_whenTargetIdIsNull() throws Exception { - mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge") + mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isBadRequest()); @@ -146,7 +147,7 @@ class TagControllerTest { when(tagService.mergeTags(any(), any())) .thenThrow(DomainException.notFound(ErrorCode.TAG_NOT_FOUND, "Tag not found")); - mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge") + mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetId\": \"" + UUID.randomUUID() + "\"}")) .andExpect(status().isNotFound()); @@ -159,7 +160,7 @@ class TagControllerTest { Tag target = Tag.builder().id(targetId).name("Target").build(); when(tagService.mergeTags(any(), any())).thenReturn(target); - mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge") + mockMvc.perform(post("/api/tags/" + UUID.randomUUID() + "/merge").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"targetId\": \"" + targetId + "\"}")) .andExpect(status().isOk()) @@ -171,21 +172,21 @@ class TagControllerTest { @Test void deleteSubtree_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree")) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void deleteSubtree_returns403_whenMissingAdminTagPermission() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree")) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree").with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "ADMIN_TAG") void deleteSubtree_returns204_whenHasAdminTagPermission() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree")) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID() + "/subtree").with(csrf())) .andExpect(status().isNoContent()); } @@ -193,21 +194,21 @@ class TagControllerTest { @Test void deleteTag_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser void deleteTag_returns403_whenMissingAdminTagPermission() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @Test @WithMockUser(authorities = "ADMIN_TAG") void deleteTag_returns200_whenHasAdminTagPermission() throws Exception { - mockMvc.perform(delete("/api/tags/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isOk()); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java index 533aa7b3..4dfd0f5b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminControllerTest.java @@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(AdminController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -83,14 +84,14 @@ class AdminControllerTest { @Test void backfillVersions_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/admin/backfill-versions")) + mockMvc.perform(post("/api/admin/backfill-versions").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(roles = "USER") void backfillVersions_returns403_whenNotAdmin() throws Exception { - mockMvc.perform(post("/api/admin/backfill-versions")) + mockMvc.perform(post("/api/admin/backfill-versions").with(csrf())) .andExpect(status().isForbidden()); } @@ -100,7 +101,7 @@ class AdminControllerTest { when(documentService.getDocumentsWithoutVersions()).thenReturn(List.of(Document.builder().build())); when(documentVersionService.backfillMissingVersions(anyList())).thenReturn(1); - mockMvc.perform(post("/api/admin/backfill-versions")) + mockMvc.perform(post("/api/admin/backfill-versions").with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.count").value(1)); } @@ -109,14 +110,14 @@ class AdminControllerTest { @Test void backfillFileHashes_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/admin/backfill-file-hashes")) + mockMvc.perform(post("/api/admin/backfill-file-hashes").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(roles = "USER") void backfillFileHashes_returns403_whenNotAdmin() throws Exception { - mockMvc.perform(post("/api/admin/backfill-file-hashes")) + mockMvc.perform(post("/api/admin/backfill-file-hashes").with(csrf())) .andExpect(status().isForbidden()); } @@ -125,7 +126,7 @@ class AdminControllerTest { void backfillFileHashes_returns200_withCount_whenAdmin() throws Exception { when(documentService.backfillFileHashes()).thenReturn(3); - mockMvc.perform(post("/api/admin/backfill-file-hashes")) + mockMvc.perform(post("/api/admin/backfill-file-hashes").with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.count").value(3)); } @@ -134,14 +135,14 @@ class AdminControllerTest { @Test void generateThumbnails_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/admin/generate-thumbnails")) + mockMvc.perform(post("/api/admin/generate-thumbnails").with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(roles = "USER") void generateThumbnails_returns403_whenNotAdmin() throws Exception { - mockMvc.perform(post("/api/admin/generate-thumbnails")) + mockMvc.perform(post("/api/admin/generate-thumbnails").with(csrf())) .andExpect(status().isForbidden()); } @@ -152,7 +153,7 @@ class AdminControllerTest { ThumbnailBackfillService.State.RUNNING, "running…", 10, 0, 0, 0, LocalDateTime.now()); when(thumbnailBackfillService.getStatus()).thenReturn(status); - mockMvc.perform(post("/api/admin/generate-thumbnails")) + mockMvc.perform(post("/api/admin/generate-thumbnails").with(csrf())) .andExpect(status().isAccepted()) .andExpect(jsonPath("$.state").value("RUNNING")) .andExpect(jsonPath("$.total").value(10)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/AuthControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/AuthControllerTest.java index 7a752c3d..5f92cce3 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/user/AuthControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/AuthControllerTest.java @@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(AuthController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -117,7 +118,7 @@ class AuthControllerTest { req.setFirstName("Max"); req.setLastName("Muster"); - mockMvc.perform(post("/api/auth/register") + mockMvc.perform(post("/api/auth/register").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isCreated()) @@ -134,7 +135,7 @@ class AuthControllerTest { req.setEmail("dupe@test.com"); req.setPassword("password123"); - mockMvc.perform(post("/api/auth/register") + mockMvc.perform(post("/api/auth/register").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isConflict()); @@ -150,7 +151,7 @@ class AuthControllerTest { req.setEmail("new@test.com"); req.setPassword("abc"); - mockMvc.perform(post("/api/auth/register") + mockMvc.perform(post("/api/auth/register").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isBadRequest()); @@ -166,7 +167,7 @@ class AuthControllerTest { req.setEmail("new@test.com"); req.setPassword("password123"); - mockMvc.perform(post("/api/auth/register") + mockMvc.perform(post("/api/auth/register").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isNotFound()); @@ -183,7 +184,7 @@ class AuthControllerTest { req.setPassword("password123"); // No WithMockUser — must still succeed (no auth challenge) - mockMvc.perform(post("/api/auth/register") + mockMvc.perform(post("/api/auth/register").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isCreated()); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/InviteControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/InviteControllerTest.java index 03b2e641..401fd83d 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/user/InviteControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/InviteControllerTest.java @@ -33,6 +33,7 @@ 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(InviteController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -103,7 +104,7 @@ class InviteControllerTest { @Test void createInvite_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/invites") + mockMvc.perform(post("/api/invites").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isUnauthorized()); @@ -112,7 +113,7 @@ class InviteControllerTest { @Test @WithMockUser(username = "user@test.com") void createInvite_returns403_whenUserLacksAdminUserPermission() throws Exception { - mockMvc.perform(post("/api/invites") + mockMvc.perform(post("/api/invites").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isForbidden()); @@ -142,7 +143,7 @@ class InviteControllerTest { req.setLabel("Für Familie"); req.setMaxUses(1); - mockMvc.perform(post("/api/invites") + mockMvc.perform(post("/api/invites").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(req))) .andExpect(status().isCreated()) @@ -164,7 +165,7 @@ class InviteControllerTest { .thenReturn(makeInviteDTO(savedToken.getId(), "ABCDE12345")); String body = "{\"groupIds\":[\"" + groupId + "\"]}"; - mockMvc.perform(post("/api/invites") + mockMvc.perform(post("/api/invites").with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isCreated()); @@ -178,14 +179,14 @@ class InviteControllerTest { @Test void revokeInvite_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/invites/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/invites/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(username = "user@test.com") void revokeInvite_returns403_whenUserLacksAdminUserPermission() throws Exception { - mockMvc.perform(delete("/api/invites/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/invites/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @@ -194,7 +195,7 @@ class InviteControllerTest { void revokeInvite_returns204_whenSuccessful() throws Exception { UUID id = UUID.randomUUID(); - mockMvc.perform(delete("/api/invites/" + id)) + mockMvc.perform(delete("/api/invites/" + id).with(csrf())) .andExpect(status().isNoContent()); verify(inviteService).revokeInvite(id); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/UserControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/UserControllerTest.java index 06a5d611..ccdda803 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/user/UserControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/UserControllerTest.java @@ -24,6 +24,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @WebMvcTest(UserController.class) @Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) @@ -83,7 +84,7 @@ class UserControllerTest { @Test @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"}) void createUser_returns400_whenEmailIsNotValidEmailFormat() throws Exception { - mockMvc.perform(post("/api/users") + mockMvc.perform(post("/api/users").with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{\"email\":\"notanemail\",\"initialPassword\":\"secret123\"}")) .andExpect(status().isBadRequest()); @@ -92,7 +93,7 @@ class UserControllerTest { @Test @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"}) void createUser_returns400_whenEmailContainsColon() throws Exception { - mockMvc.perform(post("/api/users") + mockMvc.perform(post("/api/users").with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{\"email\":\"user:name@example.com\",\"initialPassword\":\"secret123\"}")) .andExpect(status().isBadRequest()); @@ -101,7 +102,7 @@ class UserControllerTest { @Test @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"}) void createUser_returns400_whenEmailIsBlank() throws Exception { - mockMvc.perform(post("/api/users") + mockMvc.perform(post("/api/users").with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{\"email\":\"\",\"initialPassword\":\"secret123\"}")) .andExpect(status().isBadRequest()); @@ -112,7 +113,7 @@ class UserControllerTest { @Test @WithMockUser(username = "reader@example.com") void createUser_returns403_whenCallerLacksAdminUserPermission() throws Exception { - mockMvc.perform(post("/api/users") + mockMvc.perform(post("/api/users").with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{\"email\":\"x@x.com\",\"initialPassword\":\"secret123\"}")) .andExpect(status().isForbidden()); @@ -121,7 +122,7 @@ class UserControllerTest { @Test @WithMockUser(username = "reader@example.com") void adminUpdateUser_returns403_whenCallerLacksAdminUserPermission() throws Exception { - mockMvc.perform(put("/api/users/" + UUID.randomUUID()) + mockMvc.perform(put("/api/users/" + UUID.randomUUID()).with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isForbidden()); @@ -130,7 +131,7 @@ class UserControllerTest { @Test @WithMockUser(username = "reader@example.com") void deleteUser_returns403_whenCallerLacksAdminUserPermission() throws Exception { - mockMvc.perform(delete("/api/users/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/users/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isForbidden()); } @@ -138,7 +139,7 @@ class UserControllerTest { @Test void createUser_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/users") + mockMvc.perform(post("/api/users").with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{\"email\":\"x@x.com\",\"initialPassword\":\"secret123\"}")) .andExpect(status().isUnauthorized()); @@ -146,7 +147,7 @@ class UserControllerTest { @Test void adminUpdateUser_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(put("/api/users/" + UUID.randomUUID()) + mockMvc.perform(put("/api/users/" + UUID.randomUUID()).with(csrf()) .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isUnauthorized()); @@ -154,7 +155,7 @@ class UserControllerTest { @Test void deleteUser_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/users/" + UUID.randomUUID())) + mockMvc.perform(delete("/api/users/" + UUID.randomUUID()).with(csrf())) .andExpect(status().isUnauthorized()); } }