feat(auth): remove username field, migrate identity to email

- AppUser entity: replace username with email (NOT NULL, UNIQUE,
  colon-pattern validated)
- AppUserRepository: remove findByUsername, rename search JPQL to
  searchByEmailOrName (searches email + firstName + lastName)
- CreateUserRequest: remove username, require email with colon guard
- UserService: rename findByUsername→findByEmail, createUserOrUpdate
  upserts by email, blank-email guard throws instead of setting null
- UserController + all other controllers: findByEmail(auth.getName())
- DataInitializer: email-based config and lookup, E2E users have email
- V44 migration: pre-check + email NOT NULL + drop username column
- All tests updated: .username() builders removed, mocks updated,
  NotificationRepositoryTest fixtures include email fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 20:49:15 +02:00
parent 10e980328d
commit e8039bca5a
28 changed files with 247 additions and 265 deletions

View File

@@ -31,8 +31,8 @@ import java.util.Set;
@DependsOn("flyway") @DependsOn("flyway")
public class DataInitializer { public class DataInitializer {
@Value("${app.admin.username:admin}") @Value("${app.admin.email:admin@familyarchive.local}")
private String adminUsername; private String adminEmail;
@Value("${app.admin.password:admin123}") @Value("${app.admin.password:admin123}")
private String adminPassword; private String adminPassword;
@@ -43,26 +43,23 @@ public class DataInitializer {
@Bean @Bean
public CommandLineRunner initAdminUser(PasswordEncoder passwordEncoder) { public CommandLineRunner initAdminUser(PasswordEncoder passwordEncoder) {
return args -> { return args -> {
if (userRepository.findByUsername(adminUsername).isEmpty()) { if (userRepository.findByEmail(adminEmail).isEmpty()) {
log.info("Kein Admin-User '{}' gefunden. Erstelle Default-Admin...", adminUsername); log.info("Kein Admin-User '{}' gefunden. Erstelle Default-Admin...", adminEmail);
// 1. Admin Gruppe erstellen
UserGroup adminGroup = UserGroup.builder() UserGroup adminGroup = UserGroup.builder()
.name("Administrators") .name("Administrators")
.permissions(Set.of("ADMIN", "READ_ALL", "WRITE_ALL", "ANNOTATE_ALL", "ADMIN_USER", "ADMIN_TAG", "ADMIN_PERMISSION")) .permissions(Set.of("ADMIN", "READ_ALL", "WRITE_ALL", "ANNOTATE_ALL", "ADMIN_USER", "ADMIN_TAG", "ADMIN_PERMISSION"))
.build(); .build();
groupRepository.save(adminGroup); groupRepository.save(adminGroup);
// 2. Admin User erstellen
AppUser admin = AppUser.builder() AppUser admin = AppUser.builder()
.username(adminUsername) .email(adminEmail)
.password(passwordEncoder.encode(adminPassword)) // Passwort verschlüsseln! .password(passwordEncoder.encode(adminPassword))
.email("admin@familyarchive.local")
.groups(Set.of(adminGroup)) .groups(Set.of(adminGroup))
.build(); .build();
userRepository.save(admin); userRepository.save(admin);
log.info("Default Admin erstellt: User='{}'", adminUsername); log.info("Default Admin erstellt: Email='{}'", adminEmail);
} }
}; };
} }
@@ -84,16 +81,13 @@ public class DataInitializer {
TagRepository tagRepo, TagRepository tagRepo,
PasswordEncoder passwordEncoder) { PasswordEncoder passwordEncoder) {
return args -> { return args -> {
// Always reset the admin password to the configured value so a failed password-reset userRepository.findByEmail(adminEmail).ifPresent(admin -> {
// test from a previous run can never leave the account locked out.
userRepository.findByUsername(adminUsername).ifPresent(admin -> {
admin.setPassword(passwordEncoder.encode(adminPassword)); admin.setPassword(passwordEncoder.encode(adminPassword));
userRepository.save(admin); userRepository.save(admin);
log.info("E2E seed: Admin-Passwort auf konfigurierten Wert zurückgesetzt."); log.info("E2E seed: Admin-Passwort auf konfigurierten Wert zurückgesetzt.");
}); });
// Always ensure the read-only test user exists, even when seed data was already loaded. if (userRepository.findByEmail("reader@familyarchive.local").isEmpty()) {
if (userRepository.findByUsername("reader").isEmpty()) {
log.info("E2E seed: Erstelle 'reader'-Testbenutzer..."); log.info("E2E seed: Erstelle 'reader'-Testbenutzer...");
UserGroup leserGroup = groupRepository.findByName("Leser").orElseGet(() -> UserGroup leserGroup = groupRepository.findByName("Leser").orElseGet(() ->
groupRepository.save(UserGroup.builder() groupRepository.save(UserGroup.builder()
@@ -101,7 +95,7 @@ public class DataInitializer {
.permissions(Set.of("READ_ALL")) .permissions(Set.of("READ_ALL"))
.build())); .build()));
userRepository.save(AppUser.builder() userRepository.save(AppUser.builder()
.username("reader") .email("reader@familyarchive.local")
.password(passwordEncoder.encode("reader123")) .password(passwordEncoder.encode("reader123"))
.groups(Set.of(leserGroup)) .groups(Set.of(leserGroup))
.build()); .build());
@@ -131,7 +125,6 @@ public class DataInitializer {
Tag tagUrlaub = tagRepo.save(Tag.builder().name("Urlaub").build()); Tag tagUrlaub = tagRepo.save(Tag.builder().name("Urlaub").build());
// ── Documents ──────────────────────────────────────────────────── // ── Documents ────────────────────────────────────────────────────
// 1. Fully transcribed letter — used by search + detail E2E tests
docRepo.save(Document.builder() docRepo.save(Document.builder()
.title("Geburtsurkunde Hans Müller") .title("Geburtsurkunde Hans Müller")
.originalFilename("geburtsurkunde_hans.pdf") .originalFilename("geburtsurkunde_hans.pdf")
@@ -144,7 +137,6 @@ public class DataInitializer {
.transcription("Hiermit wird beurkundet, dass Hans Müller am 12. April 1923 in Berlin geboren wurde.") .transcription("Hiermit wird beurkundet, dass Hans Müller am 12. April 1923 in Berlin geboren wurde.")
.build()); .build());
// 2. Letter with multiple receivers and tags — tests multi-receiver display
docRepo.save(Document.builder() docRepo.save(Document.builder()
.title("Brief aus dem Krieg") .title("Brief aus dem Krieg")
.originalFilename("brief_krieg_1944.pdf") .originalFilename("brief_krieg_1944.pdf")
@@ -157,7 +149,6 @@ public class DataInitializer {
.transcription("Liebe Anna, ich schreibe dir aus der Front. Es geht mir den Umständen entsprechend gut.") .transcription("Liebe Anna, ich schreibe dir aus der Front. Es geht mir den Umständen entsprechend gut.")
.build()); .build());
// 3. Postcard — no transcription, tests PLACEHOLDER status
docRepo.save(Document.builder() docRepo.save(Document.builder()
.title("Urlaubspostkarte Ostsee") .title("Urlaubspostkarte Ostsee")
.originalFilename("postkarte_1965.jpg") .originalFilename("postkarte_1965.jpg")
@@ -169,7 +160,6 @@ public class DataInitializer {
.tags(Set.of(tagUrlaub)) .tags(Set.of(tagUrlaub))
.build()); .build());
// 4. Document with no sender — tests null-sender display ("Unbekannt")
docRepo.save(Document.builder() docRepo.save(Document.builder()
.title("Unbekanntes Dokument") .title("Unbekanntes Dokument")
.originalFilename("unbekannt.pdf") .originalFilename("unbekannt.pdf")
@@ -179,7 +169,6 @@ public class DataInitializer {
.receivers(Set.of(maria)) .receivers(Set.of(maria))
.build()); .build());
// 5. Document with minimal metadata — tests sparse display
docRepo.save(Document.builder() docRepo.save(Document.builder()
.title("Scan ohne Titel") .title("Scan ohne Titel")
.originalFilename("scan_ohne_titel.pdf") .originalFilename("scan_ohne_titel.pdf")

View File

@@ -72,7 +72,7 @@ public class AnnotationController {
private UUID resolveUserId(Authentication authentication) { private UUID resolveUserId(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) return null; if (authentication == null || !authentication.isAuthenticated()) return null;
try { try {
AppUser user = userService.findByUsername(authentication.getName()); AppUser user = userService.findByEmail(authentication.getName());
return user != null ? user.getId() : null; return user != null ? user.getId() : null;
} catch (Exception e) { } catch (Exception e) {
log.warn("Could not resolve user for annotation: {}", e.getMessage()); log.warn("Could not resolve user for annotation: {}", e.getMessage());

View File

@@ -144,7 +144,7 @@ public class CommentController {
private AppUser resolveUser(Authentication authentication) { private AppUser resolveUser(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) return null; if (authentication == null || !authentication.isAuthenticated()) return null;
try { try {
return userService.findByUsername(authentication.getName()); return userService.findByEmail(authentication.getName());
} catch (Exception e) { } catch (Exception e) {
log.warn("Could not resolve user for comment: {}", e.getMessage()); log.warn("Could not resolve user for comment: {}", e.getMessage());
return null; return null;

View File

@@ -100,6 +100,6 @@ public class NotificationController {
// ─── private helpers ────────────────────────────────────────────────────── // ─── private helpers ──────────────────────────────────────────────────────
private AppUser resolveUser(Authentication authentication) { private AppUser resolveUser(Authentication authentication) {
return userService.findByUsername(authentication.getName()); return userService.findByEmail(authentication.getName());
} }
} }

View File

@@ -161,7 +161,7 @@ public class OcrController {
private UUID resolveUserId(Authentication authentication) { private UUID resolveUserId(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) return null; if (authentication == null || !authentication.isAuthenticated()) return null;
try { try {
AppUser user = userService.findByUsername(authentication.getName()); AppUser user = userService.findByEmail(authentication.getName());
return user != null ? user.getId() : null; return user != null ? user.getId() : null;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to resolve user ID for authentication: {}", authentication.getName(), e); log.warn("Failed to resolve user ID for authentication: {}", authentication.getName(), e);

View File

@@ -101,7 +101,7 @@ public class TranscriptionBlockController {
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
throw DomainException.unauthorized("Authentication required"); throw DomainException.unauthorized("Authentication required");
} }
AppUser user = userService.findByUsername(authentication.getName()); AppUser user = userService.findByEmail(authentication.getName());
if (user == null) { if (user == null) {
throw DomainException.unauthorized("User not found"); throw DomainException.unauthorized("User not found");
} }

View File

@@ -38,7 +38,7 @@ public class UserController {
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
} }
AppUser user = userService.findByUsername(authentication.getName()); AppUser user = userService.findByEmail(authentication.getName());
user.setPassword(null); user.setPassword(null);
return ResponseEntity.ok(user); return ResponseEntity.ok(user);
} }
@@ -46,7 +46,7 @@ public class UserController {
@PutMapping("users/me") @PutMapping("users/me")
public ResponseEntity<AppUser> updateProfile(Authentication authentication, public ResponseEntity<AppUser> updateProfile(Authentication authentication,
@RequestBody UpdateProfileDTO dto) { @RequestBody UpdateProfileDTO dto) {
AppUser current = userService.findByUsername(authentication.getName()); AppUser current = userService.findByEmail(authentication.getName());
AppUser updated = userService.updateProfile(current.getId(), dto); AppUser updated = userService.updateProfile(current.getId(), dto);
updated.setPassword(null); updated.setPassword(null);
return ResponseEntity.ok(updated); return ResponseEntity.ok(updated);
@@ -56,7 +56,7 @@ public class UserController {
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
public void changePassword(Authentication authentication, public void changePassword(Authentication authentication,
@RequestBody ChangePasswordDTO dto) { @RequestBody ChangePasswordDTO dto) {
AppUser current = userService.findByUsername(authentication.getName()); AppUser current = userService.findByEmail(authentication.getName());
userService.changePassword(current.getId(), dto); userService.changePassword(current.getId(), dto);
} }

View File

@@ -1,6 +1,7 @@
package org.raddatz.familienarchiv.dto; package org.raddatz.familienarchiv.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
import java.time.LocalDate; import java.time.LocalDate;
@@ -9,7 +10,8 @@ import java.util.UUID;
@Data @Data
public class CreateUserRequest { public class CreateUserRequest {
private String username; @NotBlank
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
private String email; private String email;
private String initialPassword; private String initialPassword;
private List<UUID> groupIds; private List<UUID> groupIds;

View File

@@ -1,6 +1,8 @@
package org.raddatz.familienarchiv.model; package org.raddatz.familienarchiv.model;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.*; import lombok.*;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
@@ -17,7 +19,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(name = "users") // Tabellenname in Postgres @Table(name = "users")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@@ -30,26 +32,25 @@ public class AppUser {
private UUID id; private UUID id;
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
@NotBlank
@Pattern(regexp = "^[^:]+$", message = "Email must not contain a colon")
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private String username; private String email;
@Column(nullable = false) @Column(nullable = false)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password; // Wird verschlüsselt gespeichert (BCrypt) private String password;
private String firstName; private String firstName;
private String lastName; private String lastName;
private LocalDate birthDate; private LocalDate birthDate;
@Column(unique = true)
private String email;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String contact; private String contact;
@Builder.Default @Builder.Default
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private boolean enabled = true; // Um User zu sperren ohne sie zu löschen private boolean enabled = true;
@Column(nullable = false) @Column(nullable = false)
@Builder.Default @Builder.Default
@@ -61,7 +62,6 @@ public class AppUser {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private boolean notifyOnMention = false; private boolean notifyOnMention = false;
// Ein User kann in mehreren Gruppen sein
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id")) @JoinTable(name = "users_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id"))
@Builder.Default @Builder.Default
@@ -77,26 +77,21 @@ public class AppUser {
return false; return false;
} }
return this.groups.stream().anyMatch(group -> group.getPermissions().contains(permission)); return this.groups.stream().anyMatch(group -> group.getPermissions().contains(permission));
} }
public AppUser updateFromRequest(CreateUserRequest request, PasswordEncoder passwordEncoder, Set<UserGroup> groups) { public AppUser updateFromRequest(CreateUserRequest request, PasswordEncoder passwordEncoder, Set<UserGroup> groups) {
if (request.getUsername() != null && !request.getUsername().isBlank()) { if (request.getEmail() != null && !request.getEmail().isBlank()) {
this.username = request.getUsername(); this.email = request.getEmail();
} }
if (request.getEmail() != null && !request.getEmail().isBlank()) { if (request.getInitialPassword() != null && !request.getInitialPassword().isBlank()) {
this.email = request.getEmail(); this.password = passwordEncoder.encode(request.getInitialPassword());
} }
if (request.getInitialPassword() != null && !request.getInitialPassword().isBlank()) { if (groups != null && !groups.isEmpty()) {
this.password = passwordEncoder.encode(request.getInitialPassword()); this.groups = groups;
} }
if (groups != null && !groups.isEmpty()) { return this;
this.groups = groups;
} }
return this;
}
} }

View File

@@ -13,11 +13,10 @@ import java.util.UUID;
@Repository @Repository
public interface AppUserRepository extends JpaRepository<AppUser, UUID> { public interface AppUserRepository extends JpaRepository<AppUser, UUID> {
Optional<AppUser> findByUsername(String username);
Optional<AppUser> findByEmail(String email); Optional<AppUser> findByEmail(String email);
@Query("SELECT u FROM AppUser u WHERE " + @Query("SELECT u FROM AppUser u WHERE " +
"LOWER(COALESCE(u.firstName, '') || ' ' || COALESCE(u.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) " + "LOWER(u.email) LIKE LOWER(CONCAT('%', :q, '%')) " +
"OR LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%'))") "OR LOWER(COALESCE(u.firstName, '') || ' ' || COALESCE(u.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%'))")
List<AppUser> searchByNameOrUsername(@Param("q") String q, Pageable pageable); List<AppUser> searchByEmailOrName(@Param("q") String q, Pageable pageable);
} }

View File

@@ -175,7 +175,7 @@ public class CommentService {
String first = author.getFirstName(); String first = author.getFirstName();
String last = author.getLastName(); String last = author.getLastName();
if ((first == null || first.isBlank()) && (last == null || last.isBlank())) { if ((first == null || first.isBlank()) && (last == null || last.isBlank())) {
return author.getUsername(); return author.getEmail();
} }
return ((first != null ? first : "") + " " + (last != null ? last : "")).strip(); return ((first != null ? first : "") + " " + (last != null ? last : "")).strip();
} }

View File

@@ -100,7 +100,7 @@ public class DocumentVersionService {
return null; return null;
} }
try { try {
return userService.findByUsername(auth.getName()); return userService.findByEmail(auth.getName());
} catch (Exception e) { } catch (Exception e) {
log.warn("Could not resolve editor for version snapshot: {}", e.getMessage()); log.warn("Could not resolve editor for version snapshot: {}", e.getMessage());
return null; return null;
@@ -114,7 +114,7 @@ public class DocumentVersionService {
if (first != null && !first.isBlank() && last != null && !last.isBlank()) { if (first != null && !first.isBlank() && last != null && !last.isBlank()) {
return first + " " + last; return first + " " + last;
} }
return user.getUsername(); return user.getEmail();
} }
private String serializeSnapshot(Document doc) { private String serializeSnapshot(Document doc) {

View File

@@ -18,6 +18,6 @@ public class UserSearchService {
public List<AppUser> search(String query) { public List<AppUser> search(String query) {
if (query == null || query.isBlank()) return List.of(); if (query == null || query.isBlank()) return List.of();
return userRepository.searchByNameOrUsername(query.trim(), PageRequest.of(0, MAX_RESULTS)); return userRepository.searchByEmailOrName(query.trim(), PageRequest.of(0, MAX_RESULTS));
} }
} }

View File

@@ -36,23 +36,22 @@ public class UserService {
@Transactional @Transactional
public AppUser createUserOrUpdate(CreateUserRequest request) { public AppUser createUserOrUpdate(CreateUserRequest request) {
log.info("Creating or updating user: {}", request.getUsername()); log.info("Creating or updating user: {}", request.getEmail());
Set<UserGroup> groups = new HashSet<>(); Set<UserGroup> groups = new HashSet<>();
if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) { if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) {
groups.addAll(groupRepository.findAllById(request.getGroupIds())); groups.addAll(groupRepository.findAllById(request.getGroupIds()));
} }
Optional<AppUser> existingUser = userRepository.findByUsername(request.getUsername()); Optional<AppUser> existingUser = userRepository.findByEmail(request.getEmail());
AppUser user; AppUser user;
if (existingUser.isPresent()) { if (existingUser.isPresent()) {
log.info("User exists, updating: {}", request.getUsername()); log.info("User exists, updating: {}", request.getEmail());
user = existingUser.get().updateFromRequest(request, passwordEncoder, groups); user = existingUser.get().updateFromRequest(request, passwordEncoder, groups);
} else { } else {
log.info("Creating new user: {}", request.getUsername()); log.info("Creating new user: {}", request.getEmail());
user = AppUser.builder() user = AppUser.builder()
.username(request.getUsername())
.email(request.getEmail()) .email(request.getEmail())
.password(passwordEncoder.encode(request.getInitialPassword())) .password(passwordEncoder.encode(request.getInitialPassword()))
.groups(groups) .groups(groups)
@@ -158,9 +157,9 @@ public class UserService {
userRepository.save(user); userRepository.save(user);
} }
public AppUser findByUsername(String username) { public AppUser findByEmail(String email) {
return userRepository.findByUsername(username) return userRepository.findByEmail(email)
.orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for username: " + username)); .orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for email: " + email));
} }
public List<AppUser> getAllUsers() { public List<AppUser> getAllUsers() {

View File

@@ -0,0 +1,11 @@
-- Abort if any user has no email address set.
-- All users must have an email before this migration can run.
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM users WHERE email IS NULL) THEN
RAISE EXCEPTION 'Migration aborted: some users have no email address. Set emails for all users before running this migration.';
END IF;
END $$;
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP COLUMN username;

View File

@@ -280,7 +280,7 @@ class AnnotationControllerTest {
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception { void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
// findByUsername throws → catch block → resolveUserId returns null // findByUsername throws → catch block → resolveUserId returns null
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
when(userService.findByUsername(any())).thenThrow(new RuntimeException("DB error")); when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error"));
DocumentAnnotation saved = DocumentAnnotation.builder() DocumentAnnotation saved = DocumentAnnotation.builder()
.id(UUID.randomUUID()).documentId(docId).pageNumber(1) .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
.x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build(); .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();
@@ -298,7 +298,7 @@ class AnnotationControllerTest {
void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception { void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
// findByUsername returns null → user != null = false → resolveUserId returns null // findByUsername returns null → user != null = false → resolveUserId returns null
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
when(userService.findByUsername(any())).thenReturn(null); when(userService.findByEmail(any())).thenReturn(null);
DocumentAnnotation saved = DocumentAnnotation.builder() DocumentAnnotation saved = DocumentAnnotation.builder()
.id(UUID.randomUUID()).documentId(docId).pageNumber(1) .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
.x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build(); .x(0.1).y(0.1).width(0.2).height(0.2).color("#ff0000").build();

View File

@@ -270,7 +270,7 @@ class CommentControllerTest {
@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 // findByUsername throws → catch block in resolveUser → author null, saves anyway
when(userService.findByUsername(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();
when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved);

View File

@@ -60,8 +60,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser") @WithMockUser(username = "testuser")
void getNotifications_returns200_whenAuthenticatedWithNoPermissions() throws Exception { void getNotifications_returns200_whenAuthenticatedWithNoPermissions() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any())) when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
.thenReturn(new PageImpl<>(List.of())); .thenReturn(new PageImpl<>(List.of()));
@@ -72,12 +72,12 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void getNotifications_returns200WithList_whenAuthenticated() throws Exception { void getNotifications_returns200WithList_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
NotificationDTO dto = new NotificationDTO( NotificationDTO dto = new NotificationDTO(
UUID.randomUUID(), NotificationType.REPLY, UUID.randomUUID(), UUID.randomUUID(), NotificationType.REPLY, UUID.randomUUID(),
UUID.randomUUID(), null, false, LocalDateTime.now(), "Anna Smith", "Testdokument"); UUID.randomUUID(), null, false, LocalDateTime.now(), "Anna Smith", "Testdokument");
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any())) when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
.thenReturn(new PageImpl<>(List.of(dto), PageRequest.of(0, 10), 1)); .thenReturn(new PageImpl<>(List.of(dto), PageRequest.of(0, 10), 1));
@@ -89,8 +89,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void getNotifications_returnsOnlyCurrentUsersNotifications() throws Exception { void getNotifications_returnsOnlyCurrentUsersNotifications() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any())) when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
.thenReturn(new PageImpl<>(List.of())); .thenReturn(new PageImpl<>(List.of()));
@@ -103,8 +103,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void getNotifications_withTypeAndReadFalse_passesFiltersToService() throws Exception { void getNotifications_withTypeAndReadFalse_passesFiltersToService() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any())) when(notificationService.getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any()))
.thenReturn(new PageImpl<>(List.of())); .thenReturn(new PageImpl<>(List.of()));
@@ -148,8 +148,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void markAllRead_returns204_whenAuthenticated() throws Exception { void markAllRead_returns204_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
mockMvc.perform(post("/api/notifications/read-all")) mockMvc.perform(post("/api/notifications/read-all"))
.andExpect(status().isNoContent()); .andExpect(status().isNoContent());
@@ -168,10 +168,10 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void markOneRead_returns403_whenNotificationBelongsToDifferentUser() throws Exception { void markOneRead_returns403_whenNotificationBelongsToDifferentUser() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
UUID notifId = UUID.randomUUID(); UUID notifId = UUID.randomUUID();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
org.mockito.Mockito.doThrow( org.mockito.Mockito.doThrow(
org.raddatz.familienarchiv.exception.DomainException.forbidden("not yours")) org.raddatz.familienarchiv.exception.DomainException.forbidden("not yours"))
.when(notificationService).markRead(notifId, USER_ID); .when(notificationService).markRead(notifId, USER_ID);
@@ -198,9 +198,9 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void getPreferences_returns200_whenUserHasReadAll() throws Exception { void getPreferences_returns200_whenUserHasReadAll() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser") AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(true).notifyOnMention(false).build(); .notifyOnReply(true).notifyOnMention(false).build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
mockMvc.perform(get("/api/users/me/notification-preferences")) mockMvc.perform(get("/api/users/me/notification-preferences"))
.andExpect(status().isOk()) .andExpect(status().isOk())
@@ -211,9 +211,9 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
void getPreferences_returns200_whenUserHasWriteAll() throws Exception { void getPreferences_returns200_whenUserHasWriteAll() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser") AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(false).notifyOnMention(true).build(); .notifyOnReply(false).notifyOnMention(true).build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
mockMvc.perform(get("/api/users/me/notification-preferences")) mockMvc.perform(get("/api/users/me/notification-preferences"))
.andExpect(status().isOk()) .andExpect(status().isOk())
@@ -223,9 +223,9 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"ANNOTATE_ALL"}) @WithMockUser(username = "testuser", authorities = {"ANNOTATE_ALL"})
void getPreferences_returns200_whenUserHasAnnotateAll() throws Exception { void getPreferences_returns200_whenUserHasAnnotateAll() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser") AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
mockMvc.perform(get("/api/users/me/notification-preferences")) mockMvc.perform(get("/api/users/me/notification-preferences"))
.andExpect(status().isOk()); .andExpect(status().isOk());
@@ -234,8 +234,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
void getNotifications_returns200_whenUserHasOnlyWriteAll() throws Exception { void getNotifications_returns200_whenUserHasOnlyWriteAll() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any())) when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
.thenReturn(new PageImpl<>(List.of())); .thenReturn(new PageImpl<>(List.of()));
@@ -248,11 +248,11 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void updatePreferences_persistsBothBooleans() throws Exception { void updatePreferences_persistsBothBooleans() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser") AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
AppUser updated = AppUser.builder().id(USER_ID).username("testuser") AppUser updated = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(true).notifyOnMention(true).build(); .notifyOnReply(true).notifyOnMention(true).build();
when(notificationService.updatePreferences(USER_ID, true, true)).thenReturn(updated); when(notificationService.updatePreferences(USER_ID, true, true)).thenReturn(updated);
@@ -267,11 +267,11 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
void updatePreferences_returns200_whenUserHasWriteAll() throws Exception { void updatePreferences_returns200_whenUserHasWriteAll() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser") AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
AppUser updated = AppUser.builder().id(USER_ID).username("testuser") AppUser updated = AppUser.builder().id(USER_ID).email("testuser@example.com")
.notifyOnReply(true).notifyOnMention(false).build(); .notifyOnReply(true).notifyOnMention(false).build();
when(notificationService.updatePreferences(USER_ID, true, false)).thenReturn(updated); when(notificationService.updatePreferences(USER_ID, true, false)).thenReturn(updated);
@@ -293,8 +293,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void countUnread_returns200WithCount_whenAuthenticated() throws Exception { void countUnread_returns200WithCount_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(notificationService.countUnread(USER_ID)).thenReturn(3L); when(notificationService.countUnread(USER_ID)).thenReturn(3L);
mockMvc.perform(get("/api/notifications/unread-count")) mockMvc.perform(get("/api/notifications/unread-count"))
@@ -316,8 +316,8 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void stream_returns200_whenAuthenticated() throws Exception { void stream_returns200_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
when(sseEmitterRegistry.register(USER_ID)).thenReturn(new org.springframework.web.servlet.mvc.method.annotation.SseEmitter()); when(sseEmitterRegistry.register(USER_ID)).thenReturn(new org.springframework.web.servlet.mvc.method.annotation.SseEmitter());
mockMvc.perform(get("/api/notifications/stream") mockMvc.perform(get("/api/notifications/stream")
@@ -330,10 +330,10 @@ class NotificationControllerTest {
@Test @Test
@WithMockUser(username = "testuser", authorities = {"READ_ALL"}) @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
void markOneRead_returns404_whenNotificationDoesNotExist() throws Exception { void markOneRead_returns404_whenNotificationDoesNotExist() throws Exception {
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
UUID notifId = UUID.randomUUID(); UUID notifId = UUID.randomUUID();
when(userService.findByUsername("testuser")).thenReturn(user); when(userService.findByEmail("testuser")).thenReturn(user);
doThrow(DomainException.notFound(ErrorCode.NOTIFICATION_NOT_FOUND, "Notification not found: " + notifId)) doThrow(DomainException.notFound(ErrorCode.NOTIFICATION_NOT_FOUND, "Notification not found: " + notifId))
.when(notificationService).markRead(notifId, USER_ID); .when(notificationService).markRead(notifId, USER_ID);

View File

@@ -54,7 +54,7 @@ class TranscriptionBlockControllerTest {
"{\"blockIds\":[\"" + UUID.randomUUID() + "\",\"" + UUID.randomUUID() + "\"]}"; "{\"blockIds\":[\"" + UUID.randomUUID() + "\",\"" + UUID.randomUUID() + "\"]}";
private AppUser mockUser() { private AppUser mockUser() {
return AppUser.builder().id(UUID.randomUUID()).username("user").build(); return AppUser.builder().id(UUID.randomUUID()).email("user@example.com").build();
} }
private TranscriptionBlock sampleBlock() { private TranscriptionBlock sampleBlock() {
@@ -161,7 +161,7 @@ class TranscriptionBlockControllerTest {
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void createBlock_returns201_withSavedBlock_whenAuthorised() throws Exception { void createBlock_returns201_withSavedBlock_whenAuthorised() throws Exception {
when(userService.findByUsername(any())).thenReturn(mockUser()); when(userService.findByEmail(any())).thenReturn(mockUser());
when(transcriptionService.createBlock(eq(DOC_ID), any(), any())).thenReturn(sampleBlock()); when(transcriptionService.createBlock(eq(DOC_ID), any(), any())).thenReturn(sampleBlock());
mockMvc.perform(post(URL_BASE) mockMvc.perform(post(URL_BASE)
@@ -175,7 +175,7 @@ class TranscriptionBlockControllerTest {
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void createBlock_returns401_whenUserNotFoundInDatabase() throws Exception { void createBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
when(userService.findByUsername(any())).thenReturn(null); when(userService.findByEmail(any())).thenReturn(null);
mockMvc.perform(post(URL_BASE) mockMvc.perform(post(URL_BASE)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -209,7 +209,7 @@ class TranscriptionBlockControllerTest {
updated.setText("Neue Fassung"); updated.setText("Neue Fassung");
updated.setLabel("Anrede"); updated.setLabel("Anrede");
when(userService.findByUsername(any())).thenReturn(mockUser()); when(userService.findByEmail(any())).thenReturn(mockUser());
when(transcriptionService.updateBlock(eq(DOC_ID), eq(BLOCK_ID), any(), any())) when(transcriptionService.updateBlock(eq(DOC_ID), eq(BLOCK_ID), any(), any()))
.thenReturn(updated); .thenReturn(updated);
@@ -224,7 +224,7 @@ class TranscriptionBlockControllerTest {
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void updateBlock_returns404_whenBlockDoesNotExist() throws Exception { void updateBlock_returns404_whenBlockDoesNotExist() throws Exception {
when(userService.findByUsername(any())).thenReturn(mockUser()); when(userService.findByEmail(any())).thenReturn(mockUser());
when(transcriptionService.updateBlock(any(), any(), any(), any())) when(transcriptionService.updateBlock(any(), any(), any(), any()))
.thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found")); .thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"));
@@ -237,7 +237,7 @@ class TranscriptionBlockControllerTest {
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void updateBlock_returns401_whenUserNotFoundInDatabase() throws Exception { void updateBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
when(userService.findByUsername(any())).thenReturn(null); when(userService.findByEmail(any())).thenReturn(null);
mockMvc.perform(put(URL_BLOCK) mockMvc.perform(put(URL_BLOCK)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)

View File

@@ -31,33 +31,32 @@ class UserControllerTest {
@MockitoBean UserService userService; @MockitoBean UserService userService;
@MockitoBean CustomUserDetailsService customUserDetailsService; @MockitoBean CustomUserDetailsService customUserDetailsService;
// ─── GET /api/users/me ──────────────────────────────────────────────────── // ─── GET /api/users/me ────────────────────────────────────────────────────────
@Test @Test
void getCurrentUser_returns401_whenUnauthenticated() throws Exception { void getCurrentUser_returns401_whenUnauthenticated() throws Exception {
// authentication == null → returns 401 (covers null/!isAuthenticated branch)
mockMvc.perform(get("/api/users/me")) mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
} }
@Test @Test
@WithMockUser(username = "anna") @WithMockUser(username = "anna@example.com")
void getCurrentUser_returns200_whenAuthenticated() throws Exception { void getCurrentUser_returns200_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser user = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
when(userService.findByUsername("anna")).thenReturn(user); when(userService.findByEmail("anna@example.com")).thenReturn(user);
mockMvc.perform(get("/api/users/me")) mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("anna")); .andExpect(jsonPath("$.email").value("anna@example.com"));
} }
// ─── GET /api/users/{id} ────────────────────────────────────────────────── // ─── GET /api/users/{id} ──────────────────────────────────────────────────
@Test @Test
@WithMockUser(username = "reader") @WithMockUser(username = "reader@example.com")
void getUser_returns403_whenCallerLacksAdminUserPermission() throws Exception { void getUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser target = AppUser.builder().id(id).username("target").build(); AppUser target = AppUser.builder().id(id).email("target@example.com").build();
when(userService.getById(id)).thenReturn(target); when(userService.getById(id)).thenReturn(target);
mockMvc.perform(get("/api/users/" + id)) mockMvc.perform(get("/api/users/" + id))
@@ -65,14 +64,14 @@ class UserControllerTest {
} }
@Test @Test
@WithMockUser(username = "admin", authorities = {"ADMIN_USER"}) @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
void getUser_returns200_whenCallerHasAdminUserPermission() throws Exception { void getUser_returns200_whenCallerHasAdminUserPermission() throws Exception {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("target").build(); AppUser user = AppUser.builder().id(id).email("target@example.com").build();
when(userService.getById(id)).thenReturn(user); when(userService.getById(id)).thenReturn(user);
mockMvc.perform(get("/api/users/" + id)) mockMvc.perform(get("/api/users/" + id))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("target")); .andExpect(jsonPath("$.email").value("target@example.com"));
} }
} }

View File

@@ -60,7 +60,7 @@ class UserSearchControllerTest {
@WithMockUser(authorities = {"READ_ALL"}) @WithMockUser(authorities = {"READ_ALL"})
void search_returns200_whenAuthenticated() throws Exception { void search_returns200_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(UUID.randomUUID()) AppUser user = AppUser.builder().id(UUID.randomUUID())
.firstName("Hans").lastName("Mueller").username("hans").build(); .firstName("Hans").lastName("Mueller").email("hans@example.com").build();
when(userSearchService.search("Hans")).thenReturn(List.of(user)); when(userSearchService.search("Hans")).thenReturn(List.of(user));
mockMvc.perform(get("/api/users/search").param("q", "Hans")) mockMvc.perform(get("/api/users/search").param("q", "Hans"))
@@ -83,7 +83,7 @@ class UserSearchControllerTest {
void search_returnsAtMostTenResults() throws Exception { void search_returnsAtMostTenResults() throws Exception {
List<AppUser> elevenUsers = IntStream.range(0, 11) List<AppUser> elevenUsers = IntStream.range(0, 11)
.mapToObj(i -> AppUser.builder().id(UUID.randomUUID()) .mapToObj(i -> AppUser.builder().id(UUID.randomUUID())
.firstName("User").lastName(String.valueOf(i)).username("u" + i).build()) .firstName("User").lastName(String.valueOf(i)).email("u" + i + "@example.com").build())
.toList(); .toList();
when(userSearchService.search(anyString())).thenReturn(elevenUsers.subList(0, 10)); when(userSearchService.search(anyString())).thenReturn(elevenUsers.subList(0, 10));

View File

@@ -31,8 +31,8 @@ class NotificationRepositoryTest {
void setUp() { void setUp() {
notificationRepository.deleteAll(); notificationRepository.deleteAll();
appUserRepository.deleteAll(); appUserRepository.deleteAll();
userA = appUserRepository.save(AppUser.builder().username("userA").password("pw").build()); userA = appUserRepository.save(AppUser.builder().email("userA@example.com").password("pw").build());
userB = appUserRepository.save(AppUser.builder().username("userB").password("pw").build()); userB = appUserRepository.save(AppUser.builder().email("userB@example.com").password("pw").build());
} }
// ─── findByRecipientIdAndTypeAndReadFalse ───────────────────────────────── // ─── findByRecipientIdAndTypeAndReadFalse ─────────────────────────────────

View File

@@ -43,7 +43,7 @@ class CommentServiceTest {
void postComment_capturesAuthorNameAtWriteTime() { void postComment_capturesAuthorNameAtWriteTime() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder() AppUser author = AppUser.builder()
.id(UUID.randomUUID()).username("hans").firstName("Hans").lastName("Müller").build(); .id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("Müller").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("Hans Müller").content("Test").build(); .id(UUID.randomUUID()).documentId(docId).authorName("Hans Müller").content("Test").build();
when(commentRepository.save(any())).thenReturn(saved); when(commentRepository.save(any())).thenReturn(saved);
@@ -56,7 +56,7 @@ class CommentServiceTest {
@Test @Test
void postComment_fallsBackToUsername_whenNamesAreBlank() { void postComment_fallsBackToUsername_whenNamesAreBlank() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("hans42").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans42@example.com").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("hans42").content("Test").build(); .id(UUID.randomUUID()).documentId(docId).authorName("hans42").content("Test").build();
when(commentRepository.save(any())).thenReturn(saved); when(commentRepository.save(any())).thenReturn(saved);
@@ -70,8 +70,8 @@ class CommentServiceTest {
void postComment_triggersNotifyMentions_whenMentionedUserIdsProvided() { void postComment_triggersNotifyMentions_whenMentionedUserIdsProvided() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID mentionedId = UUID.randomUUID(); UUID mentionedId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("hans").firstName("Hans").lastName("M").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build();
AppUser mentioned = AppUser.builder().id(mentionedId).username("anna").firstName("Anna").lastName("S").build(); AppUser mentioned = AppUser.builder().id(mentionedId).email("anna@example.com").firstName("Anna").lastName("S").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hey @Anna S").build(); .id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hey @Anna S").build();
@@ -89,7 +89,7 @@ class CommentServiceTest {
void replyToComment_throwsNotFound_whenTargetCommentMissing() { void replyToComment_throwsNotFound_whenTargetCommentMissing() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
when(commentRepository.findById(commentId)).thenReturn(Optional.empty()); when(commentRepository.findById(commentId)).thenReturn(Optional.empty());
assertThatThrownBy(() -> commentService.replyToComment(docId, commentId, "Reply", List.of(), author)) assertThatThrownBy(() -> commentService.replyToComment(docId, commentId, "Reply", List.of(), author))
@@ -104,7 +104,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
UUID replyId = UUID.randomUUID(); UUID replyId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
@@ -127,7 +127,7 @@ class CommentServiceTest {
void replyToComment_usesDirectComment_whenReplyingToTopLevel() { void replyToComment_usesDirectComment_whenReplyingToTopLevel() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
@@ -147,7 +147,7 @@ class CommentServiceTest {
void replyToComment_triggersNotifyReply_afterSave() { void replyToComment_triggersNotifyReply_afterSave() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
@@ -168,8 +168,8 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
UUID mentionedId = UUID.randomUUID(); UUID mentionedId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
AppUser mentioned = AppUser.builder().id(mentionedId).username("bob").firstName("Bob").lastName("J").build(); AppUser mentioned = AppUser.builder().id(mentionedId).email("bob@example.com").firstName("Bob").lastName("J").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
@@ -193,7 +193,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
UUID ownerId = UUID.randomUUID(); UUID ownerId = UUID.randomUUID();
AppUser other = AppUser.builder().id(UUID.randomUUID()).username("other").build(); AppUser other = AppUser.builder().id(UUID.randomUUID()).email("other@example.com").build();
DocumentComment comment = DocumentComment.builder() DocumentComment comment = DocumentComment.builder()
.id(commentId).documentId(docId).authorId(ownerId).content("Original").authorName("Hans").build(); .id(commentId).documentId(docId).authorId(ownerId).content("Original").authorName("Hans").build();
@@ -211,7 +211,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
UUID authorId = UUID.randomUUID(); UUID authorId = UUID.randomUUID();
AppUser author = AppUser.builder().id(authorId).username("hans").build(); AppUser author = AppUser.builder().id(authorId).email("hans@example.com").build();
LocalDateTime created = LocalDateTime.now().minusMinutes(5); LocalDateTime created = LocalDateTime.now().minusMinutes(5);
DocumentComment comment = DocumentComment.builder() DocumentComment comment = DocumentComment.builder()
@@ -233,7 +233,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
UUID ownerId = UUID.randomUUID(); UUID ownerId = UUID.randomUUID();
AppUser other = AppUser.builder().id(UUID.randomUUID()).username("other").build(); AppUser other = AppUser.builder().id(UUID.randomUUID()).email("other@example.com").build();
DocumentComment comment = DocumentComment.builder() DocumentComment comment = DocumentComment.builder()
.id(commentId).documentId(docId).authorId(ownerId).authorName("Hans").content("X").build(); .id(commentId).documentId(docId).authorId(ownerId).authorName("Hans").content("X").build();
@@ -251,7 +251,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
UUID authorId = UUID.randomUUID(); UUID authorId = UUID.randomUUID();
AppUser author = AppUser.builder().id(authorId).username("hans").build(); AppUser author = AppUser.builder().id(authorId).email("hans@example.com").build();
DocumentComment comment = DocumentComment.builder() DocumentComment comment = DocumentComment.builder()
.id(commentId).documentId(docId).authorId(authorId).authorName("Hans").content("X").build(); .id(commentId).documentId(docId).authorId(authorId).authorName("Hans").content("X").build();
@@ -306,7 +306,7 @@ class CommentServiceTest {
void replyToComment_handlesNullAuthorId_inExistingReply() { void replyToComment_handlesNullAuthorId_inExistingReply() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").firstName("Anna").lastName("S").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID()).content("Root").authorName("Root").build(); .id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID()).content("Root").authorName("Root").build();
@@ -331,7 +331,7 @@ class CommentServiceTest {
@Test @Test
void postComment_fallsBackToUsername_whenFirstNameBlankAndLastNameNull() { void postComment_fallsBackToUsername_whenFirstNameBlankAndLastNameNull() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName(" ").lastName(null).build(); .firstName(" ").lastName(null).build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build(); .id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build();
@@ -345,7 +345,7 @@ class CommentServiceTest {
@Test @Test
void postComment_fallsBackToUsername_whenFirstNameNullAndLastNameBlank() { void postComment_fallsBackToUsername_whenFirstNameNullAndLastNameBlank() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName(null).lastName(" ").build(); .firstName(null).lastName(" ").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build(); .id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build();
@@ -359,7 +359,7 @@ class CommentServiceTest {
@Test @Test
void postComment_includesOnlyFirstName_whenLastNameIsNull() { void postComment_includesOnlyFirstName_whenLastNameIsNull() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName("Hans").lastName(null).build(); .firstName("Hans").lastName(null).build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("Hans").content("Hi").build(); .id(UUID.randomUUID()).documentId(docId).authorName("Hans").content("Hi").build();
@@ -374,7 +374,7 @@ class CommentServiceTest {
@Test @Test
void postComment_includesOnlyLastName_whenFirstNameIsNull() { void postComment_includesOnlyLastName_whenFirstNameIsNull() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName(null).lastName("Müller").build(); .firstName(null).lastName("Müller").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("Müller").content("Hi").build(); .id(UUID.randomUUID()).documentId(docId).authorName("Müller").content("Hi").build();
@@ -391,7 +391,7 @@ class CommentServiceTest {
@Test @Test
void postComment_doesNotCallUserService_whenMentionedUserIdsIsNull() { void postComment_doesNotCallUserService_whenMentionedUserIdsIsNull() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("hans") AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com")
.firstName("Hans").lastName("M").build(); .firstName("Hans").lastName("M").build();
DocumentComment saved = DocumentComment.builder() DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hi").build(); .id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hi").build();
@@ -409,7 +409,7 @@ class CommentServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
UUID existingReplyAuthorId = UUID.randomUUID(); UUID existingReplyAuthorId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID()) .id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID())
@@ -437,7 +437,7 @@ class CommentServiceTest {
void replyToComment_excludesNullAuthorIds_fromParticipantSet() { void replyToComment_excludesNullAuthorIds_fromParticipantSet() {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID(); UUID rootId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
// Root with null authorId // Root with null authorId
DocumentComment root = DocumentComment.builder() DocumentComment root = DocumentComment.builder()
@@ -480,7 +480,7 @@ class CommentServiceTest {
private AppUser buildAdmin() { private AppUser buildAdmin() {
return AppUser.builder() return AppUser.builder()
.id(UUID.randomUUID()) .id(UUID.randomUUID())
.username("admin") .email("admin@example.com")
.groups(Set.of(UserGroup.builder() .groups(Set.of(UserGroup.builder()
.id(UUID.randomUUID()) .id(UUID.randomUUID())
.name("admins") .name("admins")
@@ -510,7 +510,7 @@ class CommentServiceTest {
void postBlockComment_setsBlockIdOnComment() { void postBlockComment_setsBlockIdOnComment() {
UUID documentId = UUID.randomUUID(); UUID documentId = UUID.randomUUID();
UUID blockId = UUID.randomUUID(); UUID blockId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("felix").firstName("Felix").lastName("Brandt").build(); AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("Brandt").build();
when(commentRepository.save(any())).thenAnswer(inv -> { when(commentRepository.save(any())).thenAnswer(inv -> {
DocumentComment c = inv.getArgument(0); DocumentComment c = inv.getArgument(0);
c.setId(UUID.randomUUID()); c.setId(UUID.randomUUID());

View File

@@ -53,8 +53,8 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_usesFirstAndLastName_whenBothPresent() { void recordVersion_usesFirstAndLastName_whenBothPresent() {
authenticateAs("emma"); authenticateAs("emma");
when(userService.findByUsername("emma")).thenReturn( when(userService.findByEmail("emma")).thenReturn(
AppUser.builder().id(UUID.randomUUID()).username("emma") AppUser.builder().id(UUID.randomUUID()).email("emma@example.com")
.firstName("Emma").lastName("Müller").build()); .firstName("Emma").lastName("Müller").build());
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -67,10 +67,10 @@ class DocumentVersionServiceTest {
} }
@Test @Test
void recordVersion_usesUsername_whenNamesAreBlank() { void recordVersion_usesEmail_whenNamesAreBlank() {
authenticateAs("otto99"); authenticateAs("otto99");
when(userService.findByUsername("otto99")).thenReturn( when(userService.findByEmail("otto99")).thenReturn(
AppUser.builder().id(UUID.randomUUID()).username("otto99") AppUser.builder().id(UUID.randomUUID()).email("otto99@example.com")
.firstName(null).lastName(null).build()); .firstName(null).lastName(null).build());
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -79,7 +79,7 @@ class DocumentVersionServiceTest {
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class); ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
verify(versionRepository).save(captor.capture()); verify(versionRepository).save(captor.capture());
assertThat(captor.getValue().getEditorName()).isEqualTo("otto99"); assertThat(captor.getValue().getEditorName()).isEqualTo("otto99@example.com");
} }
// ─── recordVersion — snapshot ───────────────────────────────────────────── // ─── recordVersion — snapshot ─────────────────────────────────────────────
@@ -87,7 +87,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_savesSnapshotContainingTitle() { void recordVersion_savesSnapshotContainingTitle() {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -110,7 +110,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_changedFieldsIsEmpty_forFirstVersion() { void recordVersion_changedFieldsIsEmpty_forFirstVersion() {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -124,7 +124,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_includesTitleInChangedFields_whenTitleChanged() throws Exception { void recordVersion_includesTitleInChangedFields_whenTitleChanged() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Document oldDoc = Document.builder().id(UUID.randomUUID()).title("Alt").build(); Document oldDoc = Document.builder().id(UUID.randomUUID()).title("Alt").build();
@@ -154,7 +154,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_doesNotIncludeUnchangedFields_inChangedFields() throws Exception { void recordVersion_doesNotIncludeUnchangedFields_inChangedFields() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -181,7 +181,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksSenderChange() throws Exception { void recordVersion_tracksSenderChange() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -209,7 +209,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksReceiverChange() throws Exception { void recordVersion_tracksReceiverChange() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -236,7 +236,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksTagChange() throws Exception { void recordVersion_tracksTagChange() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -410,7 +410,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_usesUnknown_whenUserServiceThrows() { void recordVersion_usesUnknown_whenUserServiceThrows() {
authenticateAs("missinguser"); authenticateAs("missinguser");
when(userService.findByUsername("missinguser")).thenThrow(new RuntimeException("not found")); when(userService.findByEmail("missinguser")).thenThrow(new RuntimeException("not found"));
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -424,10 +424,10 @@ class DocumentVersionServiceTest {
// ─── recordVersion — buildEditorName edge cases ─────────────────────────── // ─── recordVersion — buildEditorName edge cases ───────────────────────────
@Test @Test
void recordVersion_usesUsername_whenFirstNameIsNotBlankButLastNameIsNull() { void recordVersion_usesEmail_whenFirstNameIsNotBlankButLastNameIsNull() {
authenticateAs("user42"); authenticateAs("user42");
when(userService.findByUsername("user42")).thenReturn( when(userService.findByEmail("user42")).thenReturn(
AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName("Hans").lastName(null).build()); .firstName("Hans").lastName(null).build());
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -436,14 +436,14 @@ class DocumentVersionServiceTest {
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class); ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
verify(versionRepository).save(captor.capture()); verify(versionRepository).save(captor.capture());
assertThat(captor.getValue().getEditorName()).isEqualTo("user42"); assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
} }
@Test @Test
void recordVersion_usesUsername_whenFirstNameIsBlankButLastNameIsPresent() { void recordVersion_usesEmail_whenFirstNameIsBlankButLastNameIsPresent() {
authenticateAs("user42"); authenticateAs("user42");
when(userService.findByUsername("user42")).thenReturn( when(userService.findByEmail("user42")).thenReturn(
AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName(" ").lastName("Müller").build()); .firstName(" ").lastName("Müller").build());
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -452,14 +452,14 @@ class DocumentVersionServiceTest {
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class); ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
verify(versionRepository).save(captor.capture()); verify(versionRepository).save(captor.capture());
assertThat(captor.getValue().getEditorName()).isEqualTo("user42"); assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
} }
@Test @Test
void recordVersion_usesUsername_whenLastNameIsBlankButFirstNameIsPresent() { void recordVersion_usesEmail_whenLastNameIsBlankButFirstNameIsPresent() {
authenticateAs("user42"); authenticateAs("user42");
when(userService.findByUsername("user42")).thenReturn( when(userService.findByEmail("user42")).thenReturn(
AppUser.builder().id(UUID.randomUUID()).username("user42") AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
.firstName("Hans").lastName(" ").build()); .firstName("Hans").lastName(" ").build());
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of()); when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -468,7 +468,7 @@ class DocumentVersionServiceTest {
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class); ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
verify(versionRepository).save(captor.capture()); verify(versionRepository).save(captor.capture());
assertThat(captor.getValue().getEditorName()).isEqualTo("user42"); assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
} }
// ─── recordVersion — computeChangedFields with corrupt snapshot ────────── // ─── recordVersion — computeChangedFields with corrupt snapshot ──────────
@@ -476,7 +476,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_returnsEmptyChangedFields_whenPreviousSnapshotIsInvalidJson() { void recordVersion_returnsEmptyChangedFields_whenPreviousSnapshotIsInvalidJson() {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
DocumentVersion previous = DocumentVersion.builder() DocumentVersion previous = DocumentVersion.builder()
@@ -499,7 +499,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksSenderAdded_whenPreviousHadNoSender() throws Exception { void recordVersion_tracksSenderAdded_whenPreviousHadNoSender() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -525,7 +525,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksReceiversAdded_whenPreviousHadNone() throws Exception { void recordVersion_tracksReceiversAdded_whenPreviousHadNone() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -551,7 +551,7 @@ class DocumentVersionServiceTest {
@Test @Test
void recordVersion_tracksTagsAdded_whenPreviousHadNone() throws Exception { void recordVersion_tracksTagsAdded_whenPreviousHadNone() throws Exception {
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -580,7 +580,7 @@ class DocumentVersionServiceTest {
void recordVersion_senderChangedToPresent_whenPreviousSenderHasNullId() throws Exception { void recordVersion_senderChangedToPresent_whenPreviousSenderHasNullId() throws Exception {
// Covers: prevSender instanceof Map = true, but id == null → prevId = null // Covers: prevSender instanceof Map = true, but id == null → prevId = null
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
// Manually craft a JSON where sender object exists but id is null // Manually craft a JSON where sender object exists but id is null
@@ -610,7 +610,7 @@ class DocumentVersionServiceTest {
void recordVersion_doesNotTrackSender_whenSenderUnchanged() throws Exception { void recordVersion_doesNotTrackSender_whenSenderUnchanged() throws Exception {
// Covers: !Objects.equals(currentId, prevId) = false → don't add "sender" // Covers: !Objects.equals(currentId, prevId) = false → don't add "sender"
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -641,7 +641,7 @@ class DocumentVersionServiceTest {
void recordVersion_tracksDocumentDate_whenCurrentDocHasNonNullDate() throws Exception { void recordVersion_tracksDocumentDate_whenCurrentDocHasNonNullDate() throws Exception {
// current.getDocumentDate() != null = true → ternary true branch in computeChangedFields // current.getDocumentDate() != null = true → ternary true branch in computeChangedFields
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
@@ -671,7 +671,7 @@ class DocumentVersionServiceTest {
void recordVersion_tracksReceivers_whenPreviousSnapshotHasNullReceivers() throws Exception { void recordVersion_tracksReceivers_whenPreviousSnapshotHasNullReceivers() throws Exception {
// prevReceivers NOT instanceof List<?> → prevIds = Set.of() → if currentIds differ → added // prevReceivers NOT instanceof List<?> → prevIds = Set.of() → if currentIds differ → added
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
// Craft snapshot where "receivers" is JSON null → deserialized as null, NOT a List // Craft snapshot where "receivers" is JSON null → deserialized as null, NOT a List
@@ -697,7 +697,7 @@ class DocumentVersionServiceTest {
void recordVersion_tracksTags_whenPreviousSnapshotHasNullTags() throws Exception { void recordVersion_tracksTags_whenPreviousSnapshotHasNullTags() throws Exception {
// prevTags NOT instanceof List<?> → prevNames = Set.of() → if currentNames differ → added // prevTags NOT instanceof List<?> → prevNames = Set.of() → if currentNames differ → added
authenticateAs("user1"); authenticateAs("user1");
when(userService.findByUsername("user1")).thenReturn(stubUser("user1")); when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
// Craft snapshot where "tags" is JSON null → deserialized as null, NOT a List // Craft snapshot where "tags" is JSON null → deserialized as null, NOT a List
@@ -741,8 +741,8 @@ class DocumentVersionServiceTest {
new UsernamePasswordAuthenticationToken(username, null, List.of())); new UsernamePasswordAuthenticationToken(username, null, List.of()));
} }
private AppUser stubUser(String username) { private AppUser stubUser(String email) {
return AppUser.builder().id(UUID.randomUUID()).username(username) return AppUser.builder().id(UUID.randomUUID()).email(email)
.firstName(null).lastName(null).build(); .firstName(null).lastName(null).build();
} }

View File

@@ -50,13 +50,13 @@ class NotificationServiceTest {
void setUp() { void setUp() {
notificationService = new NotificationService(notificationRepository, userService, documentService, Optional.of(mailSender), sseEmitterRegistry); notificationService = new NotificationService(notificationRepository, userService, documentService, Optional.of(mailSender), sseEmitterRegistry);
userA = AppUser.builder().id(UUID.randomUUID()).username("userA") userA = AppUser.builder().id(UUID.randomUUID()).email("userA@example.com")
.firstName("Anna").lastName("Smith").email("a@test.com") .firstName("Anna").lastName("Smith").email("a@test.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
userB = AppUser.builder().id(UUID.randomUUID()).username("userB") userB = AppUser.builder().id(UUID.randomUUID()).email("userB@example.com")
.firstName("Bob").lastName("Jones").email("b@test.com") .firstName("Bob").lastName("Jones").email("b@test.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
userC = AppUser.builder().id(UUID.randomUUID()).username("userC") userC = AppUser.builder().id(UUID.randomUUID()).email("userC@example.com")
.firstName("Clara").lastName("Doe").email("c@test.com") .firstName("Clara").lastName("Doe").email("c@test.com")
.notifyOnReply(false).notifyOnMention(false).build(); .notifyOnReply(false).notifyOnMention(false).build();
} }

View File

@@ -42,7 +42,7 @@ class PasswordResetServiceTest {
private AppUser makeUser(String email) { private AppUser makeUser(String email) {
return AppUser.builder() return AppUser.builder()
.id(UUID.randomUUID()) .id(UUID.randomUUID())
.username("testuser") .email("testuser@example.com")
.email(email) .email(email)
.password("hashed") .password("hashed")
.build(); .build();

View File

@@ -32,7 +32,7 @@ class UserSearchServiceTest {
List<AppUser> result = userSearchService.search(null); List<AppUser> result = userSearchService.search(null);
assertThat(result).isEmpty(); assertThat(result).isEmpty();
verify(userRepository, never()).searchByNameOrUsername(any(), any()); verify(userRepository, never()).searchByEmailOrName(any(), any());
} }
@Test @Test
@@ -40,28 +40,28 @@ class UserSearchServiceTest {
List<AppUser> result = userSearchService.search(" "); List<AppUser> result = userSearchService.search(" ");
assertThat(result).isEmpty(); assertThat(result).isEmpty();
verify(userRepository, never()).searchByNameOrUsername(any(), any()); verify(userRepository, never()).searchByEmailOrName(any(), any());
} }
@Test @Test
void search_delegatesToRepository_whenQueryIsNonBlank() { void search_delegatesToRepository_whenQueryIsNonBlank() {
AppUser user = AppUser.builder().id(UUID.randomUUID()).username("hans").build(); AppUser user = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").build();
when(userRepository.searchByNameOrUsername(eq("hans"), any(PageRequest.class))) when(userRepository.searchByEmailOrName(eq("hans"), any(PageRequest.class)))
.thenReturn(List.of(user)); .thenReturn(List.of(user));
List<AppUser> result = userSearchService.search("hans"); List<AppUser> result = userSearchService.search("hans");
assertThat(result).containsExactly(user); assertThat(result).containsExactly(user);
verify(userRepository).searchByNameOrUsername(eq("hans"), any(PageRequest.class)); verify(userRepository).searchByEmailOrName(eq("hans"), any(PageRequest.class));
} }
@Test @Test
void search_trimsQuery_beforeDelegating() { void search_trimsQuery_beforeDelegating() {
when(userRepository.searchByNameOrUsername(eq("hans"), any(PageRequest.class))) when(userRepository.searchByEmailOrName(eq("hans"), any(PageRequest.class)))
.thenReturn(List.of()); .thenReturn(List.of());
userSearchService.search(" hans "); userSearchService.search(" hans ");
verify(userRepository).searchByNameOrUsername(eq("hans"), any(PageRequest.class)); verify(userRepository).searchByEmailOrName(eq("hans"), any(PageRequest.class));
} }
} }

View File

@@ -35,22 +35,22 @@ class UserServiceTest {
@Mock PasswordEncoder passwordEncoder; @Mock PasswordEncoder passwordEncoder;
@InjectMocks UserService userService; @InjectMocks UserService userService;
// ─── findByUsername ─────────────────────────────────────────────────────── // ─── findByEmail ──────────────────────────────────────────────────────────
@Test @Test
void findByUsername_throwsNotFound_whenMissing() { void findByEmail_throwsNotFound_whenMissing() {
when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty()); when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.findByUsername("ghost")) assertThatThrownBy(() -> userService.findByEmail("ghost@example.com"))
.isInstanceOf(DomainException.class); .isInstanceOf(DomainException.class);
} }
@Test @Test
void findByUsername_returnsUser_whenFound() { void findByEmail_returnsUser_whenFound() {
AppUser user = AppUser.builder().id(UUID.randomUUID()).username("admin").build(); AppUser user = AppUser.builder().id(UUID.randomUUID()).email("admin@example.com").build();
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user)); when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user));
assertThat(userService.findByUsername("admin")).isEqualTo(user); assertThat(userService.findByEmail("admin@example.com")).isEqualTo(user);
} }
// ─── deleteUser ─────────────────────────────────────────────────────────── // ─── deleteUser ───────────────────────────────────────────────────────────
@@ -67,7 +67,7 @@ class UserServiceTest {
@Test @Test
void deleteUser_deletesUser_whenFound() { void deleteUser_deletesUser_whenFound() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("gast").build(); AppUser user = AppUser.builder().id(id).email("gast@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
userService.deleteUser(id); userService.deleteUser(id);
@@ -80,14 +80,13 @@ class UserServiceTest {
@Test @Test
void createUserOrUpdate_createsNewUser_whenNotExists() { void createUserOrUpdate_createsNewUser_whenNotExists() {
CreateUserRequest req = new CreateUserRequest(); CreateUserRequest req = new CreateUserRequest();
req.setUsername("newuser");
req.setEmail("new@example.com"); req.setEmail("new@example.com");
req.setInitialPassword("secret"); req.setInitialPassword("secret");
req.setGroupIds(List.of()); req.setGroupIds(List.of());
when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("secret")).thenReturn("encoded"); when(passwordEncoder.encode("secret")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).username("newuser").build(); AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
when(userRepository.save(any())).thenReturn(saved); when(userRepository.save(any())).thenReturn(saved);
AppUser result = userService.createUserOrUpdate(req); AppUser result = userService.createUserOrUpdate(req);
@@ -99,19 +98,17 @@ class UserServiceTest {
@Test @Test
void createUserOrUpdate_updatesExistingUser_whenFound() { void createUserOrUpdate_updatesExistingUser_whenFound() {
CreateUserRequest req = new CreateUserRequest(); CreateUserRequest req = new CreateUserRequest();
req.setUsername("existing");
req.setEmail("existing@example.com"); req.setEmail("existing@example.com");
req.setInitialPassword("newpass"); req.setInitialPassword("newpass");
req.setGroupIds(List.of()); req.setGroupIds(List.of());
AppUser existing = AppUser.builder().id(UUID.randomUUID()).username("existing").build(); AppUser existing = AppUser.builder().id(UUID.randomUUID()).email("existing@example.com").build();
when(userRepository.findByUsername("existing")).thenReturn(Optional.of(existing)); when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(existing));
when(passwordEncoder.encode(any())).thenReturn("encoded"); when(passwordEncoder.encode(any())).thenReturn("encoded");
when(userRepository.save(any())).thenReturn(existing); when(userRepository.save(any())).thenReturn(existing);
userService.createUserOrUpdate(req); userService.createUserOrUpdate(req);
// save called once with the updated existing user (no new user created)
verify(userRepository, times(1)).save(existing); verify(userRepository, times(1)).save(existing);
} }
@@ -129,7 +126,7 @@ class UserServiceTest {
@Test @Test
void getById_returnsUser_whenFound() { void getById_returnsUser_whenFound() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
assertThat(userService.getById(id)).isEqualTo(user); assertThat(userService.getById(id)).isEqualTo(user);
@@ -140,7 +137,7 @@ class UserServiceTest {
@Test @Test
void updateProfile_updatesFields() { void updateProfile_updatesFields() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.empty()); when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.empty());
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -158,8 +155,8 @@ class UserServiceTest {
void updateProfile_throwsConflict_whenEmailTakenByAnotherUser() { void updateProfile_throwsConflict_whenEmailTakenByAnotherUser() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
UUID otherId = UUID.randomUUID(); UUID otherId = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
AppUser other = AppUser.builder().id(otherId).username("anna").email("taken@example.com").build(); AppUser other = AppUser.builder().id(otherId).email("taken@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other)); when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other));
@@ -174,7 +171,7 @@ class UserServiceTest {
@Test @Test
void updateProfile_allowsSameEmailForSameUser() { void updateProfile_allowsSameEmailForSameUser() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").email("max@example.com").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.of(user)); when(userRepository.findByEmail("max@example.com")).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -191,7 +188,7 @@ class UserServiceTest {
@Test @Test
void changePassword_throwsBadRequest_whenCurrentPasswordWrong() { void changePassword_throwsBadRequest_whenCurrentPasswordWrong() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").password("hashed").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").password("hashed").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(passwordEncoder.matches("wrong", "hashed")).thenReturn(false); when(passwordEncoder.matches("wrong", "hashed")).thenReturn(false);
@@ -206,7 +203,7 @@ class UserServiceTest {
@Test @Test
void changePassword_updatesHash_whenCurrentPasswordCorrect() { void changePassword_updatesHash_whenCurrentPasswordCorrect() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").password("hashed").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").password("hashed").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(passwordEncoder.matches("correct", "hashed")).thenReturn(true); when(passwordEncoder.matches("correct", "hashed")).thenReturn(true);
when(passwordEncoder.encode("newpass")).thenReturn("newHash"); when(passwordEncoder.encode("newpass")).thenReturn("newHash");
@@ -224,7 +221,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_updatesNameFields() { void adminUpdateUser_updatesNameFields() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -241,12 +238,12 @@ class UserServiceTest {
void adminUpdateUser_preservesGroups_whenGroupIdsIsNull() { void adminUpdateUser_preservesGroups_whenGroupIdsIsNull() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build(); UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build();
AppUser user = AppUser.builder().id(id).username("admin").groups(Set.of(adminGroup)).build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").groups(Set.of(adminGroup)).build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
AdminUpdateUserRequest dto = new AdminUpdateUserRequest(); AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
dto.setFirstName("Ada"); // groupIds left null → don't change groups dto.setFirstName("Ada");
AppUser result = userService.adminUpdateUser(id, dto); AppUser result = userService.adminUpdateUser(id, dto);
@@ -258,7 +255,7 @@ class UserServiceTest {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
UserGroup oldGroup = UserGroup.builder().id(UUID.randomUUID()).name("Viewers").build(); UserGroup oldGroup = UserGroup.builder().id(UUID.randomUUID()).name("Viewers").build();
UserGroup newGroup = UserGroup.builder().id(UUID.randomUUID()).name("Editors").build(); UserGroup newGroup = UserGroup.builder().id(UUID.randomUUID()).name("Editors").build();
AppUser user = AppUser.builder().id(id).username("max").groups(Set.of(oldGroup)).build(); AppUser user = AppUser.builder().id(id).email("max@example.com").groups(Set.of(oldGroup)).build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(groupRepository.findAllById(List.of(newGroup.getId()))).thenReturn(List.of(newGroup)); when(groupRepository.findAllById(List.of(newGroup.getId()))).thenReturn(List.of(newGroup));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -273,18 +270,15 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_clearsGroups_whenGroupIdsIsEmptyList() { void adminUpdateUser_clearsGroups_whenGroupIdsIsEmptyList() {
// Sending groupIds:[] is the explicit "remove from all groups" signal.
// The frontend must NEVER send [] accidentally — it must always include
// the currently-selected group checkboxes.
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build(); UserGroup adminGroup = UserGroup.builder().id(UUID.randomUUID()).name("Administrators").build();
AppUser user = AppUser.builder().id(id).username("admin").groups(Set.of(adminGroup)).build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").groups(Set.of(adminGroup)).build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(groupRepository.findAllById(List.of())).thenReturn(List.of()); when(groupRepository.findAllById(List.of())).thenReturn(List.of());
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
AdminUpdateUserRequest dto = new AdminUpdateUserRequest(); AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
dto.setGroupIds(List.of()); // empty list → intentional "remove all groups" dto.setGroupIds(List.of());
AppUser result = userService.adminUpdateUser(id, dto); AppUser result = userService.adminUpdateUser(id, dto);
@@ -308,15 +302,14 @@ class UserServiceTest {
void createUserOrUpdate_loadsGroups_whenGroupIdsNonEmpty() { void createUserOrUpdate_loadsGroups_whenGroupIdsNonEmpty() {
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins").build(); UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins").build();
CreateUserRequest req = new CreateUserRequest(); CreateUserRequest req = new CreateUserRequest();
req.setUsername("newuser");
req.setEmail("u@example.com"); req.setEmail("u@example.com");
req.setInitialPassword("pass"); req.setInitialPassword("pass");
req.setGroupIds(List.of(group.getId())); req.setGroupIds(List.of(group.getId()));
when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.empty());
when(groupRepository.findAllById(List.of(group.getId()))).thenReturn(List.of(group)); when(groupRepository.findAllById(List.of(group.getId()))).thenReturn(List.of(group));
when(passwordEncoder.encode("pass")).thenReturn("encoded"); when(passwordEncoder.encode("pass")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).username("newuser").build(); AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
when(userRepository.save(any())).thenReturn(saved); when(userRepository.save(any())).thenReturn(saved);
AppUser result = userService.createUserOrUpdate(req); AppUser result = userService.createUserOrUpdate(req);
@@ -325,7 +318,7 @@ class UserServiceTest {
verify(groupRepository).findAllById(List.of(group.getId())); verify(groupRepository).findAllById(List.of(group.getId()));
} }
// ─── updateProfile — email edge cases ───────────────────────────────────── // ─── updateProfile — blank email ──────────────────────────────────────────
@Test @Test
void updateProfile_throwsBadRequest_whenEmailIsBlank() { void updateProfile_throwsBadRequest_whenEmailIsBlank() {
@@ -344,12 +337,12 @@ class UserServiceTest {
@Test @Test
void updateProfile_doesNotChangeEmail_whenEmailDtoIsNull() { void updateProfile_doesNotChangeEmail_whenEmailDtoIsNull() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").email("keep@example.com").build(); AppUser user = AppUser.builder().id(id).email("keep@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
UpdateProfileDTO dto = new UpdateProfileDTO(); UpdateProfileDTO dto = new UpdateProfileDTO();
dto.setEmail(null); // null — no change dto.setEmail(null);
AppUser result = userService.updateProfile(id, dto); AppUser result = userService.updateProfile(id, dto);
@@ -359,7 +352,7 @@ class UserServiceTest {
@Test @Test
void updateProfile_setsContactToNull_whenContactIsBlank() { void updateProfile_setsContactToNull_whenContactIsBlank() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -376,7 +369,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_setsPassword_whenNewPasswordProvided() { void adminUpdateUser_setsPassword_whenNewPasswordProvided() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").password("old").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").password("old").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(passwordEncoder.encode("newSecret")).thenReturn("newHashed"); when(passwordEncoder.encode("newSecret")).thenReturn("newHashed");
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -392,7 +385,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_doesNotChangePassword_whenNewPasswordIsBlank() { void adminUpdateUser_doesNotChangePassword_whenNewPasswordIsBlank() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").password("original").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").password("original").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -423,8 +416,8 @@ class UserServiceTest {
void adminUpdateUser_throwsConflict_whenEmailTakenByAnotherUser() { void adminUpdateUser_throwsConflict_whenEmailTakenByAnotherUser() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
UUID otherId = UUID.randomUUID(); UUID otherId = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
AppUser other = AppUser.builder().id(otherId).username("anna").email("taken@example.com").build(); AppUser other = AppUser.builder().id(otherId).email("taken@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other)); when(userRepository.findByEmail("taken@example.com")).thenReturn(Optional.of(other));
@@ -494,14 +487,13 @@ class UserServiceTest {
@Test @Test
void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsEmpty() { void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsEmpty() {
CreateUserRequest req = new CreateUserRequest(); CreateUserRequest req = new CreateUserRequest();
req.setUsername("newuser");
req.setEmail("u@example.com"); req.setEmail("u@example.com");
req.setInitialPassword("pass"); req.setInitialPassword("pass");
req.setGroupIds(List.of()); // empty, not null req.setGroupIds(List.of());
when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); when(userRepository.findByEmail("u@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("pass")).thenReturn("encoded"); when(passwordEncoder.encode("pass")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).username("newuser").build(); AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
when(userRepository.save(any())).thenReturn(saved); when(userRepository.save(any())).thenReturn(saved);
userService.createUserOrUpdate(req); userService.createUserOrUpdate(req);
@@ -509,12 +501,12 @@ class UserServiceTest {
verify(groupRepository, never()).findAllById(any()); verify(groupRepository, never()).findAllById(any());
} }
// ─── updateProfile — contact null ───────────────────────────────────────── // ─── updateProfile — contact ──────────────────────────────────────────────
@Test @Test
void updateProfile_setsTrimmedContact_whenContactIsNonBlank() { void updateProfile_setsTrimmedContact_whenContactIsNonBlank() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -529,7 +521,7 @@ class UserServiceTest {
@Test @Test
void updateProfile_setsNullContact_whenContactIsNull() { void updateProfile_setsNullContact_whenContactIsNull() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").contact("old contact").build(); AppUser user = AppUser.builder().id(id).email("max@example.com").contact("old contact").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -544,15 +536,14 @@ class UserServiceTest {
@Test @Test
void updateProfile_allowsSameEmail_whenEmailBelongsToSameUser() { void updateProfile_allowsSameEmail_whenEmailBelongsToSameUser() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("max").email("me@example.com").build(); AppUser user = AppUser.builder().id(id).email("me@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user)); // same user when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
UpdateProfileDTO dto = new UpdateProfileDTO(); UpdateProfileDTO dto = new UpdateProfileDTO();
dto.setEmail("me@example.com"); dto.setEmail("me@example.com");
// Must not throw
AppUser result = userService.updateProfile(id, dto); AppUser result = userService.updateProfile(id, dto);
assertThat(result.getEmail()).isEqualTo("me@example.com"); assertThat(result.getEmail()).isEqualTo("me@example.com");
} }
@@ -562,7 +553,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_setsNullContact_whenContactIsNull() { void adminUpdateUser_setsNullContact_whenContactIsNull() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").contact("old contact").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").contact("old contact").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -577,7 +568,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_setsNullContact_whenContactIsBlank() { void adminUpdateUser_setsNullContact_whenContactIsBlank() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").contact("old").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").contact("old").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -592,7 +583,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_setsTrimmedContact_whenContactIsNonBlank() { void adminUpdateUser_setsTrimmedContact_whenContactIsNonBlank() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").build(); AppUser user = AppUser.builder().id(id).email("admin@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -607,7 +598,7 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_doesNotModifyEmail_whenEmailIsNull() { void adminUpdateUser_doesNotModifyEmail_whenEmailIsNull() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").email("keep@example.com").build(); AppUser user = AppUser.builder().id(id).email("keep@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -622,15 +613,14 @@ class UserServiceTest {
@Test @Test
void adminUpdateUser_allowsSameEmail_whenEmailBelongsToSameUser() { void adminUpdateUser_allowsSameEmail_whenEmailBelongsToSameUser() {
UUID id = UUID.randomUUID(); UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("admin").email("me@example.com").build(); AppUser user = AppUser.builder().id(id).email("me@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user)); when(userRepository.findById(id)).thenReturn(Optional.of(user));
when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user)); // same user when(userRepository.findByEmail("me@example.com")).thenReturn(Optional.of(user));
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
AdminUpdateUserRequest dto = new AdminUpdateUserRequest(); AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
dto.setEmail("me@example.com"); dto.setEmail("me@example.com");
// Must not throw
AppUser result = userService.adminUpdateUser(id, dto); AppUser result = userService.adminUpdateUser(id, dto);
assertThat(result.getEmail()).isEqualTo("me@example.com"); assertThat(result.getEmail()).isEqualTo("me@example.com");
} }
@@ -639,16 +629,14 @@ class UserServiceTest {
@Test @Test
void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsNull() { void createUserOrUpdate_doesNotLoadGroups_whenGroupIdsIsNull() {
// request.getGroupIds() == null → short-circuit (A=false), groupRepository never called
CreateUserRequest req = new CreateUserRequest(); CreateUserRequest req = new CreateUserRequest();
req.setUsername("nullgroups");
req.setEmail("ng@example.com"); req.setEmail("ng@example.com");
req.setInitialPassword("pass"); req.setInitialPassword("pass");
req.setGroupIds(null); // null → first condition false → short-circuit req.setGroupIds(null);
when(userRepository.findByUsername("nullgroups")).thenReturn(Optional.empty()); when(userRepository.findByEmail("ng@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("pass")).thenReturn("encoded"); when(passwordEncoder.encode("pass")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).username("nullgroups").build(); AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("ng@example.com").build();
when(userRepository.save(any())).thenReturn(saved); when(userRepository.save(any())).thenReturn(saved);
userService.createUserOrUpdate(req); userService.createUserOrUpdate(req);