feat(audit): emit USER_CREATED when admin creates a new user

Adds USER_CREATED, USER_DELETED, GROUP_MEMBERSHIP_CHANGED to AuditKind.
Injects AuditService into UserService; changes createUserOrUpdate to
accept actorId and emits logAfterCommit(USER_CREATED) only on the
new-user branch. Updates UserController to resolve and pass actorId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-26 14:48:35 +02:00
committed by marcel
parent 5062513ae6
commit e7c7f801c9
4 changed files with 83 additions and 10 deletions

View File

@@ -2,9 +2,12 @@ package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.dto.AdminUpdateUserRequest;
import org.raddatz.familienarchiv.dto.ChangePasswordDTO;
import org.raddatz.familienarchiv.dto.CreateUserRequest;
@@ -34,6 +37,7 @@ class UserServiceTest {
@Mock AppUserRepository userRepository;
@Mock UserGroupRepository groupRepository;
@Mock PasswordEncoder passwordEncoder;
@Mock AuditService auditService;
@InjectMocks UserService userService;
// ─── findByEmail ──────────────────────────────────────────────────────────
@@ -90,7 +94,7 @@ class UserServiceTest {
AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
when(userRepository.save(any())).thenReturn(saved);
AppUser result = userService.createUserOrUpdate(req);
AppUser result = userService.createUserOrUpdate(UUID.randomUUID(), req);
assertThat(result).isEqualTo(saved);
verify(userRepository).save(any());
@@ -108,7 +112,7 @@ class UserServiceTest {
when(passwordEncoder.encode(any())).thenReturn("encoded");
when(userRepository.save(any())).thenReturn(existing);
userService.createUserOrUpdate(req);
userService.createUserOrUpdate(UUID.randomUUID(), req);
verify(userRepository, times(1)).save(existing);
}
@@ -313,7 +317,7 @@ class UserServiceTest {
AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
when(userRepository.save(any())).thenReturn(saved);
AppUser result = userService.createUserOrUpdate(req);
AppUser result = userService.createUserOrUpdate(UUID.randomUUID(), req);
assertThat(result).isEqualTo(saved);
verify(groupRepository).findAllById(List.of(group.getId()));
@@ -497,7 +501,7 @@ class UserServiceTest {
AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("u@example.com").build();
when(userRepository.save(any())).thenReturn(saved);
userService.createUserOrUpdate(req);
userService.createUserOrUpdate(UUID.randomUUID(), req);
verify(groupRepository, never()).findAllById(any());
}
@@ -640,7 +644,7 @@ class UserServiceTest {
AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("ng@example.com").build();
when(userRepository.save(any())).thenReturn(saved);
userService.createUserOrUpdate(req);
userService.createUserOrUpdate(UUID.randomUUID(), req);
verify(groupRepository, never()).findAllById(any());
}
@@ -699,6 +703,52 @@ class UserServiceTest {
assertThat(result).containsExactly(g);
}
// ─── audit: USER_CREATED ──────────────────────────────────────────────────
@Test
void createUserOrUpdate_logsUserCreated_whenUserIsNew() {
UUID actorId = UUID.randomUUID();
CreateUserRequest req = new CreateUserRequest();
req.setEmail("new@example.com");
req.setInitialPassword("secret");
req.setGroupIds(List.of());
when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("secret")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).email("new@example.com").build();
when(userRepository.save(any())).thenReturn(saved);
userService.createUserOrUpdate(actorId, req);
@SuppressWarnings("unchecked")
ArgumentCaptor<java.util.Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(java.util.Map.class);
verify(auditService).logAfterCommit(
org.mockito.ArgumentMatchers.eq(AuditKind.USER_CREATED),
org.mockito.ArgumentMatchers.eq(actorId),
org.mockito.ArgumentMatchers.isNull(),
payloadCaptor.capture());
assertThat(payloadCaptor.getValue()).containsKey("userId");
assertThat(payloadCaptor.getValue()).containsEntry("email", "new@example.com");
}
@Test
void createUserOrUpdate_doesNotLogUserCreated_whenUserAlreadyExists() {
UUID actorId = UUID.randomUUID();
CreateUserRequest req = new CreateUserRequest();
req.setEmail("existing@example.com");
req.setInitialPassword("pass");
req.setGroupIds(List.of());
AppUser existing = AppUser.builder().id(UUID.randomUUID()).email("existing@example.com").build();
when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(existing));
when(passwordEncoder.encode(any())).thenReturn("encoded");
when(userRepository.save(any())).thenReturn(existing);
userService.createUserOrUpdate(actorId, req);
verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
}
// ─── createGroup ──────────────────────────────────────────────────────────
@Test