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")
public class DataInitializer {
@Value("${app.admin.username:admin}")
private String adminUsername;
@Value("${app.admin.email:admin@familyarchive.local}")
private String adminEmail;
@Value("${app.admin.password:admin123}")
private String adminPassword;
@@ -43,26 +43,23 @@ public class DataInitializer {
@Bean
public CommandLineRunner initAdminUser(PasswordEncoder passwordEncoder) {
return args -> {
if (userRepository.findByUsername(adminUsername).isEmpty()) {
log.info("Kein Admin-User '{}' gefunden. Erstelle Default-Admin...", adminUsername);
if (userRepository.findByEmail(adminEmail).isEmpty()) {
log.info("Kein Admin-User '{}' gefunden. Erstelle Default-Admin...", adminEmail);
// 1. Admin Gruppe erstellen
UserGroup adminGroup = UserGroup.builder()
.name("Administrators")
.permissions(Set.of("ADMIN", "READ_ALL", "WRITE_ALL", "ANNOTATE_ALL", "ADMIN_USER", "ADMIN_TAG", "ADMIN_PERMISSION"))
.build();
groupRepository.save(adminGroup);
// 2. Admin User erstellen
AppUser admin = AppUser.builder()
.username(adminUsername)
.password(passwordEncoder.encode(adminPassword)) // Passwort verschlüsseln!
.email("admin@familyarchive.local")
.email(adminEmail)
.password(passwordEncoder.encode(adminPassword))
.groups(Set.of(adminGroup))
.build();
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,
PasswordEncoder passwordEncoder) {
return args -> {
// Always reset the admin password to the configured value so a failed password-reset
// test from a previous run can never leave the account locked out.
userRepository.findByUsername(adminUsername).ifPresent(admin -> {
userRepository.findByEmail(adminEmail).ifPresent(admin -> {
admin.setPassword(passwordEncoder.encode(adminPassword));
userRepository.save(admin);
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.findByUsername("reader").isEmpty()) {
if (userRepository.findByEmail("reader@familyarchive.local").isEmpty()) {
log.info("E2E seed: Erstelle 'reader'-Testbenutzer...");
UserGroup leserGroup = groupRepository.findByName("Leser").orElseGet(() ->
groupRepository.save(UserGroup.builder()
@@ -101,7 +95,7 @@ public class DataInitializer {
.permissions(Set.of("READ_ALL"))
.build()));
userRepository.save(AppUser.builder()
.username("reader")
.email("reader@familyarchive.local")
.password(passwordEncoder.encode("reader123"))
.groups(Set.of(leserGroup))
.build());
@@ -131,7 +125,6 @@ public class DataInitializer {
Tag tagUrlaub = tagRepo.save(Tag.builder().name("Urlaub").build());
// ── Documents ────────────────────────────────────────────────────
// 1. Fully transcribed letter — used by search + detail E2E tests
docRepo.save(Document.builder()
.title("Geburtsurkunde Hans Müller")
.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.")
.build());
// 2. Letter with multiple receivers and tags — tests multi-receiver display
docRepo.save(Document.builder()
.title("Brief aus dem Krieg")
.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.")
.build());
// 3. Postcard — no transcription, tests PLACEHOLDER status
docRepo.save(Document.builder()
.title("Urlaubspostkarte Ostsee")
.originalFilename("postkarte_1965.jpg")
@@ -169,7 +160,6 @@ public class DataInitializer {
.tags(Set.of(tagUrlaub))
.build());
// 4. Document with no sender — tests null-sender display ("Unbekannt")
docRepo.save(Document.builder()
.title("Unbekanntes Dokument")
.originalFilename("unbekannt.pdf")
@@ -179,7 +169,6 @@ public class DataInitializer {
.receivers(Set.of(maria))
.build());
// 5. Document with minimal metadata — tests sparse display
docRepo.save(Document.builder()
.title("Scan ohne Titel")
.originalFilename("scan_ohne_titel.pdf")

View File

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

View File

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

View File

@@ -100,6 +100,6 @@ public class NotificationController {
// ─── private helpers ──────────────────────────────────────────────────────
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) {
if (authentication == null || !authentication.isAuthenticated()) return null;
try {
AppUser user = userService.findByUsername(authentication.getName());
AppUser user = userService.findByEmail(authentication.getName());
return user != null ? user.getId() : null;
} catch (Exception 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()) {
throw DomainException.unauthorized("Authentication required");
}
AppUser user = userService.findByUsername(authentication.getName());
AppUser user = userService.findByEmail(authentication.getName());
if (user == null) {
throw DomainException.unauthorized("User not found");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,6 @@ public class UserSearchService {
public List<AppUser> search(String query) {
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
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<>();
if (request.getGroupIds() != null && !request.getGroupIds().isEmpty()) {
groups.addAll(groupRepository.findAllById(request.getGroupIds()));
}
Optional<AppUser> existingUser = userRepository.findByUsername(request.getUsername());
Optional<AppUser> existingUser = userRepository.findByEmail(request.getEmail());
AppUser user;
if (existingUser.isPresent()) {
log.info("User exists, updating: {}", request.getUsername());
log.info("User exists, updating: {}", request.getEmail());
user = existingUser.get().updateFromRequest(request, passwordEncoder, groups);
} else {
log.info("Creating new user: {}", request.getUsername());
log.info("Creating new user: {}", request.getEmail());
user = AppUser.builder()
.username(request.getUsername())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getInitialPassword()))
.groups(groups)
@@ -158,9 +157,9 @@ public class UserService {
userRepository.save(user);
}
public AppUser findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for username: " + username));
public AppUser findByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for email: " + email));
}
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 {
// findByUsername throws → catch block → resolveUserId returns null
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()
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
.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 {
// findByUsername returns null → user != null = false → resolveUserId returns null
UUID docId = UUID.randomUUID();
when(userService.findByUsername(any())).thenReturn(null);
when(userService.findByEmail(any())).thenReturn(null);
DocumentAnnotation saved = DocumentAnnotation.builder()
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
.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")
void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
// 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()
.id(UUID.randomUUID()).documentId(DOC_ID).content("Test comment").build();
when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved);

View File

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

View File

@@ -54,7 +54,7 @@ class TranscriptionBlockControllerTest {
"{\"blockIds\":[\"" + UUID.randomUUID() + "\",\"" + UUID.randomUUID() + "\"]}";
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() {
@@ -161,7 +161,7 @@ class TranscriptionBlockControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
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());
mockMvc.perform(post(URL_BASE)
@@ -175,7 +175,7 @@ class TranscriptionBlockControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
void createBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
when(userService.findByUsername(any())).thenReturn(null);
when(userService.findByEmail(any())).thenReturn(null);
mockMvc.perform(post(URL_BASE)
.contentType(MediaType.APPLICATION_JSON)
@@ -209,7 +209,7 @@ class TranscriptionBlockControllerTest {
updated.setText("Neue Fassung");
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()))
.thenReturn(updated);
@@ -224,7 +224,7 @@ class TranscriptionBlockControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
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()))
.thenThrow(DomainException.notFound(ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND, "not found"));
@@ -237,7 +237,7 @@ class TranscriptionBlockControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
void updateBlock_returns401_whenUserNotFoundInDatabase() throws Exception {
when(userService.findByUsername(any())).thenReturn(null);
when(userService.findByEmail(any())).thenReturn(null);
mockMvc.perform(put(URL_BLOCK)
.contentType(MediaType.APPLICATION_JSON)

View File

@@ -31,33 +31,32 @@ class UserControllerTest {
@MockitoBean UserService userService;
@MockitoBean CustomUserDetailsService customUserDetailsService;
// ─── GET /api/users/me ────────────────────────────────────────────────────
// ─── GET /api/users/me ────────────────────────────────────────────────────────
@Test
void getCurrentUser_returns401_whenUnauthenticated() throws Exception {
// authentication == null → returns 401 (covers null/!isAuthenticated branch)
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "anna")
@WithMockUser(username = "anna@example.com")
void getCurrentUser_returns200_whenAuthenticated() throws Exception {
AppUser user = AppUser.builder().id(UUID.randomUUID()).username("anna").build();
when(userService.findByUsername("anna")).thenReturn(user);
AppUser user = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
when(userService.findByEmail("anna@example.com")).thenReturn(user);
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("anna"));
.andExpect(jsonPath("$.email").value("anna@example.com"));
}
// ─── GET /api/users/{id} ──────────────────────────────────────────────────
@Test
@WithMockUser(username = "reader")
@WithMockUser(username = "reader@example.com")
void getUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
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);
mockMvc.perform(get("/api/users/" + id))
@@ -65,14 +64,14 @@ class UserControllerTest {
}
@Test
@WithMockUser(username = "admin", authorities = {"ADMIN_USER"})
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
void getUser_returns200_whenCallerHasAdminUserPermission() throws Exception {
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);
mockMvc.perform(get("/api/users/" + id))
.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"})
void search_returns200_whenAuthenticated() throws Exception {
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));
mockMvc.perform(get("/api/users/search").param("q", "Hans"))
@@ -83,7 +83,7 @@ class UserSearchControllerTest {
void search_returnsAtMostTenResults() throws Exception {
List<AppUser> elevenUsers = IntStream.range(0, 11)
.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();
when(userSearchService.search(anyString())).thenReturn(elevenUsers.subList(0, 10));

View File

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

View File

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

View File

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

View File

@@ -50,13 +50,13 @@ class NotificationServiceTest {
void setUp() {
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")
.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")
.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")
.notifyOnReply(false).notifyOnMention(false).build();
}

View File

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

View File

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