feat: migrate from username to email-only authentication #272
@@ -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")
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class SecurityConfig {
|
|||||||
.frameOptions(frameOptions -> frameOptions.sameOrigin()))
|
.frameOptions(frameOptions -> frameOptions.sameOrigin()))
|
||||||
// Erlaubt Login via Browser-Popup oder REST-Header (Authorization: Basic ...)
|
// Erlaubt Login via Browser-Popup oder REST-Header (Authorization: Basic ...)
|
||||||
.httpBasic(Customizer.withDefaults())
|
.httpBasic(Customizer.withDefaults())
|
||||||
.formLogin(Customizer.withDefaults());
|
.formLogin(form -> form.usernameParameter("email"));
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
|
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
|
||||||
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
|
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
|
||||||
import org.raddatz.familienarchiv.dto.CreateUserRequest;
|
import org.raddatz.familienarchiv.dto.CreateUserRequest;
|
||||||
@@ -38,7 +39,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 +47,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 +57,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ public class UserController {
|
|||||||
|
|
||||||
@PostMapping("/users")
|
@PostMapping("/users")
|
||||||
@RequirePermission(Permission.ADMIN_USER)
|
@RequirePermission(Permission.ADMIN_USER)
|
||||||
public ResponseEntity<AppUser> createUser(@RequestBody CreateUserRequest request) {
|
public ResponseEntity<AppUser> createUser(@Valid @RequestBody CreateUserRequest request) {
|
||||||
return ResponseEntity.ok(userService.createUserOrUpdate(request));
|
return ResponseEntity.ok(userService.createUserOrUpdate(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.raddatz.familienarchiv.dto;
|
package org.raddatz.familienarchiv.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
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 +11,9 @@ import java.util.UUID;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class CreateUserRequest {
|
public class CreateUserRequest {
|
||||||
private String username;
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
@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;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.raddatz.familienarchiv.model;
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
@@ -17,7 +20,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 +33,26 @@ public class AppUser {
|
|||||||
private UUID id;
|
private UUID id;
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
@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 +64,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 +79,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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,24 +29,22 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
private final AppUserRepository userRepository;
|
private final AppUserRepository userRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
||||||
AppUser appUser = userRepository.findByUsername(username)
|
AppUser appUser = userRepository.findByEmail(email)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + username));
|
.orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + email));
|
||||||
|
|
||||||
// Collect all permissions from all groups; warn about any that don't match a known Permission enum value
|
|
||||||
var authorities = appUser.getGroups().stream()
|
var authorities = appUser.getGroups().stream()
|
||||||
.flatMap(group -> group.getPermissions().stream())
|
.flatMap(group -> group.getPermissions().stream())
|
||||||
.peek(p -> {
|
.peek(p -> {
|
||||||
if (!KNOWN_PERMISSIONS.contains(p)) {
|
if (!KNOWN_PERMISSIONS.contains(p)) {
|
||||||
log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getUsername());
|
log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getEmail());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(SimpleGrantedAuthority::new)
|
.map(SimpleGrantedAuthority::new)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
// Rückgabe des Standard Spring Security User Objekts
|
|
||||||
return new User(
|
return new User(
|
||||||
appUser.getUsername(),
|
appUser.getEmail(),
|
||||||
appUser.getPassword(),
|
appUser.getPassword(),
|
||||||
appUser.isEnabled(),
|
appUser.isEnabled(),
|
||||||
true, true, true,
|
true, true, true,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -103,8 +102,8 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
user.setEmail(dto.getEmail().trim());
|
user.setEmail(dto.getEmail().trim());
|
||||||
} else if (dto.getEmail() != null && dto.getEmail().isBlank()) {
|
} else if (dto.getEmail() != null) {
|
||||||
user.setEmail(null);
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "Email must not be blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.setFirstName(dto.getFirstName());
|
user.setFirstName(dto.getFirstName());
|
||||||
@@ -126,8 +125,8 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
user.setEmail(dto.getEmail().trim());
|
user.setEmail(dto.getEmail().trim());
|
||||||
} else if (dto.getEmail() != null && dto.getEmail().isBlank()) {
|
} else if (dto.getEmail() != null) {
|
||||||
user.setEmail(null);
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "Email must not be blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.setFirstName(dto.getFirstName());
|
user.setFirstName(dto.getFirstName());
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -278,9 +278,9 @@ class AnnotationControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "ANNOTATE_ALL")
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
|
void createAnnotation_resolvesNullUserId_whenUserServiceThrows() throws Exception {
|
||||||
// findByUsername throws → catch block → resolveUserId returns null
|
// findByEmail throws → catch block → resolveUserId returns null
|
||||||
UUID docId = UUID.randomUUID();
|
UUID docId = UUID.randomUUID();
|
||||||
when(userService.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();
|
||||||
@@ -296,9 +296,9 @@ class AnnotationControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "ANNOTATE_ALL")
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
|
void createAnnotation_resolvesNullUserId_whenUserServiceReturnsNull() throws Exception {
|
||||||
// findByUsername returns null → user != null = false → resolveUserId returns null
|
// findByEmail returns null → user != null = false → resolveUserId returns null
|
||||||
UUID docId = UUID.randomUUID();
|
UUID docId = UUID.randomUUID();
|
||||||
when(userService.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();
|
||||||
|
|||||||
@@ -269,8 +269,8 @@ class CommentControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "WRITE_ALL")
|
@WithMockUser(authorities = "WRITE_ALL")
|
||||||
void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
|
void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception {
|
||||||
// findByUsername throws → catch block in resolveUser → author null, saves anyway
|
// findByEmail throws → catch block in resolveUser → author null, saves anyway
|
||||||
when(userService.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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.util.UUID;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@@ -31,33 +32,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 +65,43 @@ 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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── POST /api/users ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
|
||||||
|
void createUser_returns400_whenEmailIsNotValidEmailFormat() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/users")
|
||||||
|
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"email\":\"notanemail\",\"initialPassword\":\"secret123\"}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
|
||||||
|
void createUser_returns400_whenEmailContainsColon() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/users")
|
||||||
|
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"email\":\"user:name@example.com\",\"initialPassword\":\"secret123\"}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
|
||||||
|
void createUser_returns400_whenEmailIsBlank() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/users")
|
||||||
|
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"email\":\"\",\"initialPassword\":\"secret123\"}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,34 @@ class MigrationIntegrationTest {
|
|||||||
assertThat(rows2).isEqualTo(1);
|
assertThat(rows2).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── V44: email NOT NULL constraint ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v44_emailNotNullConstraint_rejectsInsertWithNullEmail() {
|
||||||
|
assertThatThrownBy(() ->
|
||||||
|
jdbc.update("""
|
||||||
|
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||||
|
VALUES (gen_random_uuid(), NULL, 'hash', true, false, false)
|
||||||
|
""")
|
||||||
|
).isInstanceOf(DataIntegrityViolationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v44_emailUniqueConstraint_rejectsDuplicateEmail() {
|
||||||
|
String email = "unique-test-" + UUID.randomUUID() + "@example.com";
|
||||||
|
jdbc.update("""
|
||||||
|
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||||
|
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
|
||||||
|
""", email);
|
||||||
|
|
||||||
|
assertThatThrownBy(() ->
|
||||||
|
jdbc.update("""
|
||||||
|
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||||
|
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
|
||||||
|
""", email)
|
||||||
|
).isInstanceOf(DataIntegrityViolationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── helpers ─────────────────────────────────────────────────────────────
|
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private UUID createPerson(String firstName, String lastName) {
|
private UUID createPerson(String firstName, String lastName) {
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.raddatz.familienarchiv.model.AppUser;
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
import org.raddatz.familienarchiv.model.UserGroup;
|
import org.raddatz.familienarchiv.model.UserGroup;
|
||||||
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
|
||||||
@@ -29,40 +28,40 @@ class CustomUserDetailsServiceTest {
|
|||||||
// ─── loadUserByUsername — not found ──────────────────────────────────────
|
// ─── loadUserByUsername — not found ──────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_throwsUsernameNotFoundException_whenUserNotFound() {
|
void loadUserByEmail_throwsUsernameNotFoundException_whenUserNotFound() {
|
||||||
when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty());
|
when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.loadUserByUsername("ghost"))
|
assertThatThrownBy(() -> service.loadUserByUsername("ghost@example.com"))
|
||||||
.isInstanceOf(UsernameNotFoundException.class)
|
.isInstanceOf(UsernameNotFoundException.class)
|
||||||
.hasMessageContaining("ghost");
|
.hasMessageContaining("ghost@example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── loadUserByUsername — happy path ─────────────────────────────────────
|
// ─── loadUserByUsername — happy path ─────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_returnsUserDetails_withMappedAuthorities() {
|
void loadUserByEmail_returnsUserDetails_withEmailAsPrincipal() {
|
||||||
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins")
|
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins")
|
||||||
.permissions(Set.of("READ_ALL", "WRITE_ALL")).build();
|
.permissions(Set.of("READ_ALL", "WRITE_ALL")).build();
|
||||||
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
.username("admin").password("hashed").enabled(true)
|
.email("admin@example.com").password("hashed").enabled(true)
|
||||||
.groups(Set.of(group)).build();
|
.groups(Set.of(group)).build();
|
||||||
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
|
when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user));
|
||||||
|
|
||||||
UserDetails details = service.loadUserByUsername("admin");
|
UserDetails details = service.loadUserByUsername("admin@example.com");
|
||||||
|
|
||||||
assertThat(details.getUsername()).isEqualTo("admin");
|
assertThat(details.getUsername()).isEqualTo("admin@example.com");
|
||||||
assertThat(details.getAuthorities()).extracting("authority")
|
assertThat(details.getAuthorities()).extracting("authority")
|
||||||
.contains("READ_ALL", "WRITE_ALL");
|
.contains("READ_ALL", "WRITE_ALL");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_returnsEmptyAuthorities_whenUserHasNoGroups() {
|
void loadUserByEmail_returnsEmptyAuthorities_whenUserHasNoGroups() {
|
||||||
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
.username("viewer").password("hashed").enabled(true)
|
.email("viewer@example.com").password("hashed").enabled(true)
|
||||||
.groups(Set.of()).build();
|
.groups(Set.of()).build();
|
||||||
when(userRepository.findByUsername("viewer")).thenReturn(Optional.of(user));
|
when(userRepository.findByEmail("viewer@example.com")).thenReturn(Optional.of(user));
|
||||||
|
|
||||||
UserDetails details = service.loadUserByUsername("viewer");
|
UserDetails details = service.loadUserByUsername("viewer@example.com");
|
||||||
|
|
||||||
assertThat(details.getAuthorities()).isEmpty();
|
assertThat(details.getAuthorities()).isEmpty();
|
||||||
}
|
}
|
||||||
@@ -70,16 +69,15 @@ class CustomUserDetailsServiceTest {
|
|||||||
// ─── loadUserByUsername — unknown permission ──────────────────────────────
|
// ─── loadUserByUsername — unknown permission ──────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_grantsUnknownPermission_butLogsWarning() {
|
void loadUserByEmail_grantsUnknownPermission_butLogsWarning() {
|
||||||
// Unknown permissions should still be granted (logged as warning, not silently dropped)
|
|
||||||
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup")
|
UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup")
|
||||||
.permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build();
|
.permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build();
|
||||||
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
.username("custom").password("hashed").enabled(true)
|
.email("custom@example.com").password("hashed").enabled(true)
|
||||||
.groups(Set.of(group)).build();
|
.groups(Set.of(group)).build();
|
||||||
when(userRepository.findByUsername("custom")).thenReturn(Optional.of(user));
|
when(userRepository.findByEmail("custom@example.com")).thenReturn(Optional.of(user));
|
||||||
|
|
||||||
UserDetails details = service.loadUserByUsername("custom");
|
UserDetails details = service.loadUserByUsername("custom@example.com");
|
||||||
|
|
||||||
assertThat(details.getAuthorities()).extracting("authority")
|
assertThat(details.getAuthorities()).extracting("authority")
|
||||||
.contains("UNKNOWN_CUSTOM_PERM");
|
.contains("UNKNOWN_CUSTOM_PERM");
|
||||||
@@ -88,13 +86,13 @@ class CustomUserDetailsServiceTest {
|
|||||||
// ─── loadUserByUsername — disabled user ───────────────────────────────────
|
// ─── loadUserByUsername — disabled user ───────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_returnsDisabledUser_whenUserIsDisabled() {
|
void loadUserByEmail_returnsDisabledUser_whenUserIsDisabled() {
|
||||||
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
.username("disabled").password("hashed").enabled(false)
|
.email("disabled@example.com").password("hashed").enabled(false)
|
||||||
.groups(Set.of()).build();
|
.groups(Set.of()).build();
|
||||||
when(userRepository.findByUsername("disabled")).thenReturn(Optional.of(user));
|
when(userRepository.findByEmail("disabled@example.com")).thenReturn(Optional.of(user));
|
||||||
|
|
||||||
UserDetails details = service.loadUserByUsername("disabled");
|
UserDetails details = service.loadUserByUsername("disabled@example.com");
|
||||||
|
|
||||||
assertThat(details.isEnabled()).isFalse();
|
assertThat(details.isEnabled()).isFalse();
|
||||||
}
|
}
|
||||||
@@ -102,17 +100,17 @@ class CustomUserDetailsServiceTest {
|
|||||||
// ─── loadUserByUsername — multi-group permission merge ────────────────────
|
// ─── loadUserByUsername — multi-group permission merge ────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadUserByUsername_mergesPermissionsFromMultipleGroups() {
|
void loadUserByEmail_mergesPermissionsFromMultipleGroups() {
|
||||||
UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers")
|
UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers")
|
||||||
.permissions(Set.of("READ_ALL")).build();
|
.permissions(Set.of("READ_ALL")).build();
|
||||||
UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers")
|
UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers")
|
||||||
.permissions(Set.of("WRITE_ALL")).build();
|
.permissions(Set.of("WRITE_ALL")).build();
|
||||||
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
.username("multi").password("hashed").enabled(true)
|
.email("multi@example.com").password("hashed").enabled(true)
|
||||||
.groups(Set.of(g1, g2)).build();
|
.groups(Set.of(g1, g2)).build();
|
||||||
when(userRepository.findByUsername("multi")).thenReturn(Optional.of(user));
|
when(userRepository.findByEmail("multi@example.com")).thenReturn(Optional.of(user));
|
||||||
|
|
||||||
UserDetails details = service.loadUserByUsername("multi");
|
UserDetails details = service.loadUserByUsername("multi@example.com");
|
||||||
|
|
||||||
assertThat(details.getAuthorities()).extracting("authority")
|
assertThat(details.getAuthorities()).extracting("authority")
|
||||||
.containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");
|
.containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,32 +318,31 @@ 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_setsEmailToNull_whenEmailIsBlank() {
|
void updateProfile_throwsBadRequest_whenEmailIsBlank() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
AppUser user = AppUser.builder().id(id).username("max").email("old@example.com").build();
|
AppUser user = AppUser.builder().id(id).email("old@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));
|
|
||||||
|
|
||||||
UpdateProfileDTO dto = new UpdateProfileDTO();
|
UpdateProfileDTO dto = new UpdateProfileDTO();
|
||||||
dto.setEmail(" "); // blank — should clear email
|
dto.setEmail(" ");
|
||||||
|
|
||||||
AppUser result = userService.updateProfile(id, dto);
|
assertThatThrownBy(() -> userService.updateProfile(id, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
assertThat(result.getEmail()).isNull();
|
.hasMessageContaining("blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
||||||
@@ -360,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));
|
||||||
|
|
||||||
@@ -377,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));
|
||||||
@@ -393,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));
|
||||||
|
|
||||||
@@ -407,26 +399,25 @@ class UserServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void adminUpdateUser_setsEmailToNull_whenEmailIsBlank() {
|
void adminUpdateUser_throwsBadRequest_whenEmailIsBlank() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
AppUser user = AppUser.builder().id(id).username("admin").email("old@example.com").build();
|
AppUser user = AppUser.builder().id(id).email("old@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));
|
|
||||||
|
|
||||||
AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
|
AdminUpdateUserRequest dto = new AdminUpdateUserRequest();
|
||||||
dto.setEmail(" ");
|
dto.setEmail(" ");
|
||||||
|
|
||||||
AppUser result = userService.adminUpdateUser(id, dto);
|
assertThatThrownBy(() -> userService.adminUpdateUser(id, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
assertThat(result.getEmail()).isNull();
|
.hasMessageContaining("blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
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));
|
||||||
|
|
||||||
@@ -496,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);
|
||||||
@@ -511,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));
|
||||||
|
|
||||||
@@ -531,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));
|
||||||
|
|
||||||
@@ -546,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");
|
||||||
}
|
}
|
||||||
@@ -564,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));
|
||||||
|
|
||||||
@@ -579,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));
|
||||||
|
|
||||||
@@ -594,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));
|
||||||
|
|
||||||
@@ -609,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));
|
||||||
|
|
||||||
@@ -624,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");
|
||||||
}
|
}
|
||||||
@@ -641,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);
|
||||||
|
|||||||
@@ -53,8 +53,9 @@
|
|||||||
"form_placeholder_archive_location": "z.B. Schrank 3, Mappe B",
|
"form_placeholder_archive_location": "z.B. Schrank 3, Mappe B",
|
||||||
"form_helper_archive_location": "Wo befindet sich das Originaldokument?",
|
"form_helper_archive_location": "Wo befindet sich das Originaldokument?",
|
||||||
"login_heading": "Anmelden",
|
"login_heading": "Anmelden",
|
||||||
"login_label_username": "Benutzername",
|
"login_label_email": "E-Mail-Adresse",
|
||||||
"login_label_password": "Passwort",
|
"login_label_password": "Passwort",
|
||||||
|
"login_error_missing_credentials": "Bitte E-Mail-Adresse und Passwort eingeben.",
|
||||||
"login_btn_submit": "Anmelden",
|
"login_btn_submit": "Anmelden",
|
||||||
"docs_search_placeholder": "Titel, Personen, Tags durchsuchen…",
|
"docs_search_placeholder": "Titel, Personen, Tags durchsuchen…",
|
||||||
"docs_sort_label": "Sortierung",
|
"docs_sort_label": "Sortierung",
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
"admin_tab_groups": "Gruppen",
|
"admin_tab_groups": "Gruppen",
|
||||||
"admin_tab_tags": "Schlagworte",
|
"admin_tab_tags": "Schlagworte",
|
||||||
"admin_section_users": "Benutzerverwaltung",
|
"admin_section_users": "Benutzerverwaltung",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "E-Mail",
|
||||||
"admin_col_groups": "Gruppen",
|
"admin_col_groups": "Gruppen",
|
||||||
"admin_col_password": "Passwort",
|
"admin_col_password": "Passwort",
|
||||||
"admin_multiselect_hint": "Strg+Klick für Auswahl",
|
"admin_multiselect_hint": "Strg+Klick für Auswahl",
|
||||||
|
|||||||
@@ -53,8 +53,9 @@
|
|||||||
"form_placeholder_archive_location": "e.g. Cabinet 3, Folder B",
|
"form_placeholder_archive_location": "e.g. Cabinet 3, Folder B",
|
||||||
"form_helper_archive_location": "Where is the original document stored?",
|
"form_helper_archive_location": "Where is the original document stored?",
|
||||||
"login_heading": "Sign in",
|
"login_heading": "Sign in",
|
||||||
"login_label_username": "Username",
|
"login_label_email": "Email",
|
||||||
"login_label_password": "Password",
|
"login_label_password": "Password",
|
||||||
|
"login_error_missing_credentials": "Please enter your email address and password.",
|
||||||
"login_btn_submit": "Sign in",
|
"login_btn_submit": "Sign in",
|
||||||
"docs_search_placeholder": "Search title, people, tags…",
|
"docs_search_placeholder": "Search title, people, tags…",
|
||||||
"docs_sort_label": "Sort",
|
"docs_sort_label": "Sort",
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
"admin_tab_groups": "Groups",
|
"admin_tab_groups": "Groups",
|
||||||
"admin_tab_tags": "Tags",
|
"admin_tab_tags": "Tags",
|
||||||
"admin_section_users": "User management",
|
"admin_section_users": "User management",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "Email",
|
||||||
"admin_col_groups": "Groups",
|
"admin_col_groups": "Groups",
|
||||||
"admin_col_password": "Password",
|
"admin_col_password": "Password",
|
||||||
"admin_multiselect_hint": "Ctrl+Click to select",
|
"admin_multiselect_hint": "Ctrl+Click to select",
|
||||||
|
|||||||
@@ -53,8 +53,9 @@
|
|||||||
"form_placeholder_archive_location": "p.ej. Armario 3, Carpeta B",
|
"form_placeholder_archive_location": "p.ej. Armario 3, Carpeta B",
|
||||||
"form_helper_archive_location": "¿Dónde se encuentra el documento original?",
|
"form_helper_archive_location": "¿Dónde se encuentra el documento original?",
|
||||||
"login_heading": "Iniciar sesión",
|
"login_heading": "Iniciar sesión",
|
||||||
"login_label_username": "Usuario",
|
"login_label_email": "Correo electrónico",
|
||||||
"login_label_password": "Contraseña",
|
"login_label_password": "Contraseña",
|
||||||
|
"login_error_missing_credentials": "Por favor, introduzca su correo electrónico y contraseña.",
|
||||||
"login_btn_submit": "Iniciar sesión",
|
"login_btn_submit": "Iniciar sesión",
|
||||||
"docs_search_placeholder": "Buscar título, personas, etiquetas…",
|
"docs_search_placeholder": "Buscar título, personas, etiquetas…",
|
||||||
"docs_sort_label": "Ordenar",
|
"docs_sort_label": "Ordenar",
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
"admin_tab_groups": "Grupos",
|
"admin_tab_groups": "Grupos",
|
||||||
"admin_tab_tags": "Etiquetas",
|
"admin_tab_tags": "Etiquetas",
|
||||||
"admin_section_users": "Gestión de usuarios",
|
"admin_section_users": "Gestión de usuarios",
|
||||||
"admin_col_login": "Login",
|
"admin_col_login": "Correo electrónico",
|
||||||
"admin_col_groups": "Grupos",
|
"admin_col_groups": "Grupos",
|
||||||
"admin_col_password": "Contraseña",
|
"admin_col_password": "Contraseña",
|
||||||
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
|
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
|
||||||
|
|||||||
3
frontend/src/app.d.ts
vendored
3
frontend/src/app.d.ts
vendored
@@ -5,11 +5,10 @@ declare global {
|
|||||||
// Define the User structure matching your Java Entity
|
// Define the User structure matching your Java Entity
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
email?: string;
|
email: string;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
groups: {
|
groups: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type ErrorCode =
|
|||||||
| 'TAG_NOT_FOUND'
|
| 'TAG_NOT_FOUND'
|
||||||
| 'TAG_MERGE_SELF'
|
| 'TAG_MERGE_SELF'
|
||||||
| 'TAG_MERGE_INVALID_TARGET'
|
| 'TAG_MERGE_INVALID_TARGET'
|
||||||
|
| 'MISSING_CREDENTIALS'
|
||||||
| 'UNAUTHORIZED'
|
| 'UNAUTHORIZED'
|
||||||
| 'FORBIDDEN'
|
| 'FORBIDDEN'
|
||||||
| 'VALIDATION_ERROR'
|
| 'VALIDATION_ERROR'
|
||||||
@@ -118,6 +119,8 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string {
|
|||||||
return m.error_tag_merge_self();
|
return m.error_tag_merge_self();
|
||||||
case 'TAG_MERGE_INVALID_TARGET':
|
case 'TAG_MERGE_INVALID_TARGET':
|
||||||
return m.error_tag_merge_invalid_target();
|
return m.error_tag_merge_invalid_target();
|
||||||
|
case 'MISSING_CREDENTIALS':
|
||||||
|
return m.login_error_missing_credentials();
|
||||||
case 'UNAUTHORIZED':
|
case 'UNAUTHORIZED':
|
||||||
return m.error_unauthorized();
|
return m.error_unauthorized();
|
||||||
case 'FORBIDDEN':
|
case 'FORBIDDEN':
|
||||||
|
|||||||
@@ -1253,13 +1253,12 @@ export interface components {
|
|||||||
AppUser: {
|
AppUser: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
email?: string;
|
email: string;
|
||||||
contact?: string;
|
contact?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
notifyOnReply: boolean;
|
notifyOnReply: boolean;
|
||||||
@@ -1406,8 +1405,7 @@ export interface components {
|
|||||||
blockIds?: string[];
|
blockIds?: string[];
|
||||||
};
|
};
|
||||||
CreateUserRequest: {
|
CreateUserRequest: {
|
||||||
username?: string;
|
email: string;
|
||||||
email?: string;
|
|
||||||
initialPassword?: string;
|
initialPassword?: string;
|
||||||
groupIds?: string[];
|
groupIds?: string[];
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Group = {
|
|||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
email: string;
|
||||||
firstName: string | null;
|
firstName: string | null;
|
||||||
lastName: string | null;
|
lastName: string | null;
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
@@ -41,7 +41,7 @@ const filtered = $derived(
|
|||||||
searchQuery.trim() === ''
|
searchQuery.trim() === ''
|
||||||
? users
|
? users
|
||||||
: users.filter((u) =>
|
: users.filter((u) =>
|
||||||
[u.username, u.firstName, u.lastName]
|
[u.email, u.firstName, u.lastName]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((v) => v!.toLowerCase().includes(searchQuery.toLowerCase()))
|
.some((v) => v!.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ const filtered = $derived(
|
|||||||
? 'border-primary bg-primary/10 dark:bg-primary/15'
|
? 'border-primary bg-primary/10 dark:bg-primary/15'
|
||||||
: 'border-transparent hover:bg-muted'}"
|
: 'border-transparent hover:bg-muted'}"
|
||||||
>
|
>
|
||||||
<div class="text-sm font-bold text-ink">{user.username}</div>
|
<div class="text-sm font-bold text-ink">{user.email}</div>
|
||||||
{#if fullName}
|
{#if fullName}
|
||||||
<div class="mt-0.5 text-xs text-ink-3">{fullName}</div>
|
<div class="mt-0.5 text-xs text-ink-3">{fullName}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ let deleteFormEl = $state<HTMLFormElement | null>(null);
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
const confirmed = await confirm({
|
const confirmed = await confirm({
|
||||||
title: m.admin_user_delete_confirm({ username: data.editUser.username }),
|
title: m.admin_user_delete_confirm({ username: data.editUser.email }),
|
||||||
destructive: true
|
destructive: true
|
||||||
});
|
});
|
||||||
if (confirmed) deleteFormEl!.requestSubmit();
|
if (confirmed) deleteFormEl!.requestSubmit();
|
||||||
@@ -49,7 +49,7 @@ $effect(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<h2 class="flex-1 font-sans text-sm font-bold text-ink">
|
<h2 class="flex-1 font-sans text-sm font-bold text-ink">
|
||||||
{m.admin_user_edit_heading({ username: data.editUser.username })}
|
{m.admin_user_edit_heading({ username: data.editUser.email })}
|
||||||
</h2>
|
</h2>
|
||||||
<form bind:this={deleteFormEl} method="POST" action="?/delete" use:enhance>
|
<form bind:this={deleteFormEl} method="POST" action="?/delete" use:enhance>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const groups = [
|
|||||||
|
|
||||||
const makeUser = (overrides = {}) => ({
|
const makeUser = (overrides = {}) => ({
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
username: 'max',
|
|
||||||
firstName: 'Max',
|
firstName: 'Max',
|
||||||
lastName: 'Mustermann',
|
lastName: 'Mustermann',
|
||||||
email: 'max@example.com',
|
email: 'max@example.com',
|
||||||
@@ -52,9 +51,11 @@ afterEach(cleanup);
|
|||||||
// ─── Rendering ────────────────────────────────────────────────────────────────
|
// ─── Rendering ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Admin edit user page – rendering', () => {
|
describe('Admin edit user page – rendering', () => {
|
||||||
it('renders the heading with username', async () => {
|
it('renders the heading with email', async () => {
|
||||||
renderPage({ data: baseData, form: null });
|
renderPage({ data: baseData, form: null });
|
||||||
await expect.element(page.getByText(/Benutzer bearbeiten: max/i)).toBeInTheDocument();
|
await expect
|
||||||
|
.element(page.getByText(/Benutzer bearbeiten: max@example.com/i))
|
||||||
|
.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pre-fills first name from editUser data', async () => {
|
it('pre-fills first name from editUser data', async () => {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ beforeEach(() => vi.clearAllMocks());
|
|||||||
describe('admin/users layout load', () => {
|
describe('admin/users layout load', () => {
|
||||||
it('returns the users list', async () => {
|
it('returns the users list', async () => {
|
||||||
mockApi([
|
mockApi([
|
||||||
{ id: 'u1', username: 'alice' },
|
{ id: 'u1', email: 'alice@example.com' },
|
||||||
{ id: 'u2', username: 'bob' }
|
{ id: 'u2', email: 'bob@example.com' }
|
||||||
]);
|
]);
|
||||||
const result = await load({ fetch: vi.fn() as unknown as typeof fetch });
|
const result = await load({ fetch: vi.fn() as unknown as typeof fetch });
|
||||||
expect(result.users).toHaveLength(2);
|
expect(result.users).toHaveLength(2);
|
||||||
expect(result.users[0].username).toBe('alice');
|
expect(result.users[0].email).toBe('alice@example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an empty array when the API returns nothing', async () => {
|
it('returns an empty array when the API returns nothing', async () => {
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ afterEach(cleanup);
|
|||||||
const users = [
|
const users = [
|
||||||
{
|
{
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
username: 'reader',
|
email: 'reader@example.com',
|
||||||
firstName: 'Lea',
|
firstName: 'Lea',
|
||||||
lastName: 'Leserin',
|
lastName: 'Leserin',
|
||||||
groups: [{ id: 'g1', name: 'Leser', permissions: ['READ_ALL'] }]
|
groups: [{ id: 'g1', name: 'Leser', permissions: ['READ_ALL'] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'u2',
|
id: 'u2',
|
||||||
username: 'admin',
|
email: 'admin@example.com',
|
||||||
firstName: null,
|
firstName: null,
|
||||||
lastName: null,
|
lastName: null,
|
||||||
groups: [{ id: 'g2', name: 'Admins', permissions: ['ADMIN'] }]
|
groups: [{ id: 'g2', name: 'Admins', permissions: ['ADMIN'] }]
|
||||||
@@ -46,10 +46,10 @@ describe('UsersListPanel — header', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('UsersListPanel — user items', () => {
|
describe('UsersListPanel — user items', () => {
|
||||||
it('renders each username', async () => {
|
it('renders each email', async () => {
|
||||||
render(UsersListPanel, { users });
|
render(UsersListPanel, { users });
|
||||||
await expect.element(page.getByRole('link', { name: /reader/i })).toBeInTheDocument();
|
await expect.element(page.getByText('reader@example.com')).toBeInTheDocument();
|
||||||
await expect.element(page.getByRole('link', { name: /admin/i })).toBeInTheDocument();
|
await expect.element(page.getByText('admin@example.com')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('each user links to /admin/users/[id]', async () => {
|
it('each user links to /admin/users/[id]', async () => {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ export const actions: Actions = {
|
|||||||
const birthDateRaw = data.get('birthDate') as string;
|
const birthDateRaw = data.get('birthDate') as string;
|
||||||
const result = await api.POST('/api/users', {
|
const result = await api.POST('/api/users', {
|
||||||
body: {
|
body: {
|
||||||
username: data.get('username') as string,
|
email: data.get('email') as string,
|
||||||
initialPassword: data.get('password') as string,
|
initialPassword: data.get('password') as string,
|
||||||
email: (data.get('email') as string) || undefined,
|
|
||||||
groupIds: data.getAll('groupIds') as string[],
|
groupIds: data.getAll('groupIds') as string[],
|
||||||
firstName: (data.get('firstName') as string) || null,
|
firstName: (data.get('firstName') as string) || null,
|
||||||
lastName: (data.get('lastName') as string) || null,
|
lastName: (data.get('lastName') as string) || null,
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
{m.admin_col_login()}
|
{m.admin_col_login()}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
name="username"
|
name="email"
|
||||||
required
|
required
|
||||||
|
autocomplete="email"
|
||||||
class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ describe('Admin new user page – rendering', () => {
|
|||||||
await expect.element(page.getByText(/Neuen Benutzer anlegen/i)).toBeInTheDocument();
|
await expect.element(page.getByText(/Neuen Benutzer anlegen/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the login input', async () => {
|
it('renders the email input', async () => {
|
||||||
render(Page, { data: baseData, form: null });
|
render(Page, { data: baseData, form: null });
|
||||||
await expect.element(page.getByRole('textbox', { name: /Login/i })).toBeInTheDocument();
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders group checkboxes for each available group', async () => {
|
it('renders group checkboxes for each available group', async () => {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ const tick = () => new Promise((r) => setTimeout(r, 0));
|
|||||||
const makeData = (overrides = {}) => ({
|
const makeData = (overrides = {}) => ({
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
username: 'max',
|
|
||||||
firstName: 'Max',
|
firstName: 'Max',
|
||||||
lastName: 'Müller',
|
lastName: 'Müller',
|
||||||
|
email: 'max@example.com',
|
||||||
groups: [],
|
groups: [],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: ''
|
createdAt: ''
|
||||||
@@ -39,7 +39,7 @@ describe('Layout – user avatar button', () => {
|
|||||||
it('shows fallback icon button when names are not set', async () => {
|
it('shows fallback icon button when names are not set', async () => {
|
||||||
render(Layout, {
|
render(Layout, {
|
||||||
data: makeData({
|
data: makeData({
|
||||||
user: { id: '1', username: 'x', groups: [], enabled: true, createdAt: '' }
|
user: { id: '1', email: 'fallback@example.com', groups: [], enabled: true, createdAt: '' }
|
||||||
}),
|
}),
|
||||||
children: emptySnippet
|
children: emptySnippet
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { getErrorMessage } from '$lib/errors';
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
login: async ({ request, cookies, fetch }) => {
|
login: async ({ request, cookies, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const username = data.get('username') as string;
|
const email = data.get('email') as string;
|
||||||
const password = data.get('password') as string;
|
const password = data.get('password') as string;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!email || !password) {
|
||||||
return fail(400, { error: 'Bitte Benutzername und Passwort eingeben.' });
|
return fail(400, { error: getErrorMessage('MISSING_CREDENTIALS') });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = btoa(`${username}:${password}`);
|
const credentials = btoa(`${email}:${password}`);
|
||||||
const authHeader = `Basic ${credentials}`;
|
const authHeader = `Basic ${credentials}`;
|
||||||
|
|
||||||
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ let { form }: { form?: { error?: string; success?: boolean } } = $props();
|
|||||||
<form method="POST" action="?/login" class="space-y-5">
|
<form method="POST" action="?/login" class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
for="username"
|
for="email"
|
||||||
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
class="mb-1.5 block font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||||
>{m.login_label_username()}</label
|
>{m.login_label_email()}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
name="username"
|
name="email"
|
||||||
id="username"
|
id="email"
|
||||||
required
|
required
|
||||||
autocomplete="username"
|
autocomplete="email"
|
||||||
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="block w-full border border-line px-3 py-2.5 font-serif text-sm text-ink placeholder-ink-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ describe('Login page – rendering', () => {
|
|||||||
await expect.element(page.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument();
|
await expect.element(page.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the username input', async () => {
|
it('renders the email input', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ describe('Login page – rendering', () => {
|
|||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('username field is required', async () => {
|
it('email field is required', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
expect(input?.required).toBe(true);
|
expect(input?.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,6 +49,13 @@ describe('Login page – rendering', () => {
|
|||||||
expect(input?.required).toBe(true);
|
expect(input?.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('email field has type="email"', async () => {
|
||||||
|
render(LoginPage, {});
|
||||||
|
await tick();
|
||||||
|
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||||
|
expect(input?.type).toBe('email');
|
||||||
|
});
|
||||||
|
|
||||||
it('password field has type="password"', async () => {
|
it('password field has type="password"', async () => {
|
||||||
render(LoginPage, {});
|
render(LoginPage, {});
|
||||||
await tick();
|
await tick();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ let { data } = $props();
|
|||||||
const fullName = $derived.by(() => {
|
const fullName = $derived.by(() => {
|
||||||
const first = data.profileUser.firstName;
|
const first = data.profileUser.firstName;
|
||||||
const last = data.profileUser.lastName;
|
const last = data.profileUser.lastName;
|
||||||
return first || last ? [first, last].filter(Boolean).join(' ') : data.profileUser.username;
|
return first || last ? [first, last].filter(Boolean).join(' ') : data.profileUser.email;
|
||||||
});
|
});
|
||||||
|
|
||||||
const initials = $derived.by(() => {
|
const initials = $derived.by(() => {
|
||||||
@@ -70,12 +70,9 @@ const initials = $derived.by(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name and username -->
|
<!-- Name -->
|
||||||
<div class="mb-5 text-center">
|
<div class="mb-5 text-center">
|
||||||
<h2 class="font-serif text-xl font-bold text-ink">{fullName}</h2>
|
<h2 class="font-serif text-xl font-bold text-ink">{fullName}</h2>
|
||||||
{#if data.profileUser.firstName || data.profileUser.lastName}
|
|
||||||
<p class="mt-0.5 font-sans text-sm text-ink-3">@{data.profileUser.username}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Field rows -->
|
<!-- Field rows -->
|
||||||
|
|||||||
Reference in New Issue
Block a user