fix(auth): add @Email validation and @Valid to enforce email format on user creation
- 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:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user