fix(auth): add @Email validation and @Valid to enforce email format on user creation
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m20s
CI / OCR Service Tests (push) Successful in 28s
CI / Backend Unit Tests (push) Failing after 2m43s

- Add @Email annotation to CreateUserRequest.email and AppUser.email
- Add @Valid to UserController.createUser to activate bean validation
- Add MigrationIntegrationTest cases for V44 NOT NULL and UNIQUE constraints
- Fix stale test comments (findByUsername → findByEmail)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #272.
This commit is contained in:
Marcel
2026-04-18 21:52:11 +02:00
committed by marcel
parent d816e94a90
commit e1ddd66704
7 changed files with 67 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import jakarta.validation.Valid;
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
import org.raddatz.familienarchiv.dto.CreateUserRequest;
@@ -77,7 +78,7 @@ public class UserController {
@PostMapping("/users")
@RequirePermission(Permission.ADMIN_USER)
public ResponseEntity<AppUser> createUser(@RequestBody CreateUserRequest request) {
public ResponseEntity<AppUser> createUser(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.ok(userService.createUserOrUpdate(request));
}

View File

@@ -1,5 +1,6 @@
package org.raddatz.familienarchiv.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@@ -11,6 +12,7 @@ import java.util.UUID;
@Data
public class CreateUserRequest {
@NotBlank
@Email
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
private String email;
private String initialPassword;

View File

@@ -1,6 +1,7 @@
package org.raddatz.familienarchiv.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*;
@@ -33,6 +34,7 @@ public class AppUser {
@Column(unique = true, nullable = false)
@NotBlank
@Email
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private String email;

View File

@@ -278,7 +278,7 @@ class AnnotationControllerTest {
@Test
@WithMockUser(authorities = "ANNOTATE_ALL")
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
// findByUsername throws → catch block → resolveUserId returns null
// findByEmail throws → catch block → resolveUserId returns null
UUID docId = UUID.randomUUID();
when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
DocumentAnnotation saved = DocumentAnnotation.builder()
@@ -296,7 +296,7 @@ class AnnotationControllerTest {
@Test
@WithMockUser(authorities = "ANNOTATE_ALL")
void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
// findByUsername returns null → user != null = false → resolveUserId returns null
// findByEmail returns null → user != null = false → resolveUserId returns null
UUID docId = UUID.randomUUID();
when(userService.findByEmail(any())).thenReturn(null);
DocumentAnnotation saved = DocumentAnnotation.builder()

View File

@@ -269,7 +269,7 @@ class CommentControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
// findByUsername throws → catch block in resolveUser → author null, saves anyway
// findByEmail throws → catch block in resolveUser → author null, saves anyway
when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(DOC_ID).content("Test comment").build();

View File

@@ -19,6 +19,7 @@ import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -74,4 +75,33 @@ class UserControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("target@example.com"));
}
// ─── POST /api/users ──────────────────────────────────────────────────────
@Test
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
void createUser_returns400_whenEmailIsNotValidEmailFormat() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.content("{\"email\":\"notanemail\",\"initialPassword\":\"secret123\"}"))
.andExpect(status().isBadRequest());
}
@Test
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
void createUser_returns400_whenEmailContainsColon() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.content("{\"email\":\"user:name@example.com\",\"initialPassword\":\"secret123\"}"))
.andExpect(status().isBadRequest());
}
@Test
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
void createUser_returns400_whenEmailIsBlank() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.content("{\"email\":\"\",\"initialPassword\":\"secret123\"}"))
.andExpect(status().isBadRequest());
}
}

View File

@@ -274,6 +274,34 @@ class MigrationIntegrationTest {
assertThat(rows2).isEqualTo(1);
}
// ─── V44: email NOT NULL constraint ──────────────────────────────────────
@Test
void v44_emailNotNullConstraint_rejectsInsertWithNullEmail() {
assertThatThrownBy(() ->
jdbc.update("""
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
VALUES (gen_random_uuid(), NULL, 'hash', true, false, false)
""")
).isInstanceOf(DataIntegrityViolationException.class);
}
@Test
void v44_emailUniqueConstraint_rejectsDuplicateEmail() {
String email = "unique-test-" + UUID.randomUUID() + "@example.com";
jdbc.update("""
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
""", email);
assertThatThrownBy(() ->
jdbc.update("""
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
""", email)
).isInstanceOf(DataIntegrityViolationException.class);
}
// ─── helpers ─────────────────────────────────────────────────────────────
private UUID createPerson(String firstName, String lastName) {