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.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
|
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
|
||||||
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
|
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
|
||||||
import org.raddatz.familienarchiv.dto.CreateUserRequest;
|
import org.raddatz.familienarchiv.dto.CreateUserRequest;
|
||||||
@@ -77,7 +78,7 @@ public class UserController {
|
|||||||
|
|
||||||
@PostMapping("/users")
|
@PostMapping("/users")
|
||||||
@RequirePermission(Permission.ADMIN_USER)
|
@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));
|
return ResponseEntity.ok(userService.createUserOrUpdate(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.raddatz.familienarchiv.dto;
|
package org.raddatz.familienarchiv.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -11,6 +12,7 @@ import java.util.UUID;
|
|||||||
@Data
|
@Data
|
||||||
public class CreateUserRequest {
|
public class CreateUserRequest {
|
||||||
@NotBlank
|
@NotBlank
|
||||||
|
@Email
|
||||||
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
|
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
|
||||||
private String email;
|
private String email;
|
||||||
private String initialPassword;
|
private String initialPassword;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.raddatz.familienarchiv.model;
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@@ -33,6 +34,7 @@ public class AppUser {
|
|||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
@NotBlank
|
@NotBlank
|
||||||
|
@Email
|
||||||
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
|
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String email;
|
private String email;
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ class AnnotationControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "ANNOTATE_ALL")
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
|
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
|
||||||
// findByUsername throws → catch block → resolveUserId returns null
|
// findByEmail throws → catch block → resolveUserId returns null
|
||||||
UUID docId = UUID.randomUUID();
|
UUID docId = UUID.randomUUID();
|
||||||
when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
|
when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
|
||||||
DocumentAnnotation saved = DocumentAnnotation.builder()
|
DocumentAnnotation saved = DocumentAnnotation.builder()
|
||||||
@@ -296,7 +296,7 @@ class AnnotationControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "ANNOTATE_ALL")
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
|
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();
|
UUID docId = UUID.randomUUID();
|
||||||
when(userService.findByEmail(any())).thenReturn(null);
|
when(userService.findByEmail(any())).thenReturn(null);
|
||||||
DocumentAnnotation saved = DocumentAnnotation.builder()
|
DocumentAnnotation saved = DocumentAnnotation.builder()
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ class CommentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "WRITE_ALL")
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
|
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"));
|
when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
|
||||||
DocumentComment saved = DocumentComment.builder()
|
DocumentComment saved = DocumentComment.builder()
|
||||||
.id(UUID.randomUUID()).documentId(DOC_ID).content("Test comment").build();
|
.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.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.when;
|
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.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.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@@ -74,4 +75,33 @@ class UserControllerTest {
|
|||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.email").value("target@example.com"));
|
.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);
|
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 ─────────────────────────────────────────────────────────────
|
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private UUID createPerson(String firstName, String lastName) {
|
private UUID createPerson(String firstName, String lastName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user