test(backend): add service unit tests and controller slice tests
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled

Service unit tests (Mockito, no Spring context):
- DocumentServiceTest — getById, updateDocument, deleteTagCascading, createPlaceholder
- PersonServiceTest — getById, findOrCreateByAlias, mergePersons
- TagServiceTest — getById, findOrCreate, update, delete
- UserServiceTest — findByUsername, deleteUser, createUserOrUpdate, getGroupById

Controller slice tests (@WebMvcTest):
- DocumentControllerTest — 401/403/200 for GET /search, POST /, PUT /{id}
- TagControllerTest — 401/403/200 for GET, PUT /{id}, DELETE /{id}

Also removes FamilienarchivApplicationTests (full @SpringBootTest
requires DB + S3 infrastructure; covered by e2e job instead).

65 tests total, all passing.
Refs #4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-17 21:33:24 +01:00
parent ded5c24c40
commit 225d6e44c9
7 changed files with 739 additions and 13 deletions

View File

@@ -1,13 +0,0 @@
package org.raddatz.familienarchiv;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class FamilienarchivApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,116 @@
package org.raddatz.familienarchiv.controller;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.raddatz.familienarchiv.config.SecurityConfig;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Collections;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
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.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(DocumentController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class DocumentControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean DocumentService documentService;
@MockitoBean FileService fileService;
@MockitoBean CustomUserDetailsService customUserDetailsService;
// ─── GET /api/documents/search ────────────────────────────────────────────
@Test
void search_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(get("/api/documents/search"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void search_returns200_whenAuthenticated() throws Exception {
when(documentService.searchDocuments(any(), any(), any(), any(), any(), any()))
.thenReturn(Collections.emptyList());
mockMvc.perform(get("/api/documents/search"))
.andExpect(status().isOk());
}
// ─── POST /api/documents ─────────────────────────────────────────────────
@Test
void createDocument_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(multipart("/api/documents"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void createDocument_returns403_whenMissingWritePermission() throws Exception {
mockMvc.perform(multipart("/api/documents"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void createDocument_returns200_whenHasWritePermission() throws Exception {
Document doc = Document.builder()
.id(UUID.randomUUID())
.title("Test")
.originalFilename("test.pdf")
.build();
when(documentService.createDocument(any(), any())).thenReturn(doc);
mockMvc.perform(multipart("/api/documents"))
.andExpect(status().isOk());
}
// ─── PUT /api/documents/{id} ─────────────────────────────────────────────
@Test
void updateDocument_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID())
.with(req -> { req.setMethod("PUT"); return req; }))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void updateDocument_returns403_whenMissingWritePermission() throws Exception {
mockMvc.perform(multipart("/api/documents/" + UUID.randomUUID())
.with(req -> { req.setMethod("PUT"); return req; }))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void updateDocument_returns200_whenHasWritePermission() throws Exception {
UUID id = UUID.randomUUID();
Document doc = Document.builder()
.id(id)
.title("Updated")
.originalFilename("test.pdf")
.build();
when(documentService.updateDocument(any(), any(), any())).thenReturn(doc);
mockMvc.perform(multipart("/api/documents/" + id)
.with(req -> { req.setMethod("PUT"); return req; }))
.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,106 @@
package org.raddatz.familienarchiv.controller;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.raddatz.familienarchiv.config.SecurityConfig;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(TagController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class TagControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean TagService tagService;
@MockitoBean DocumentService documentService;
@MockitoBean CustomUserDetailsService customUserDetailsService;
// ─── GET /api/tags ────────────────────────────────────────────────────────
@Test
void searchTags_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(get("/api/tags"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void searchTags_returns200_whenAuthenticated() throws Exception {
when(tagService.search(any())).thenReturn(List.of());
mockMvc.perform(get("/api/tags"))
.andExpect(status().isOk());
}
// ─── PUT /api/tags/{id} ───────────────────────────────────────────────────
@Test
void updateTag_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(put("/api/tags/" + UUID.randomUUID())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"New\"}"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void updateTag_returns403_whenMissingAdminTagPermission() throws Exception {
mockMvc.perform(put("/api/tags/" + UUID.randomUUID())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"New\"}"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "ADMIN_TAG")
void updateTag_returns200_whenHasAdminTagPermission() throws Exception {
Tag tag = Tag.builder().id(UUID.randomUUID()).name("New").build();
when(tagService.update(any(), any())).thenReturn(tag);
mockMvc.perform(put("/api/tags/" + UUID.randomUUID())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"New\"}"))
.andExpect(status().isOk());
}
// ─── DELETE /api/tags/{id} ────────────────────────────────────────────────
@Test
void deleteTag_returns401_whenUnauthenticated() throws Exception {
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void deleteTag_returns403_whenMissingAdminTagPermission() throws Exception {
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "ADMIN_TAG")
void deleteTag_returns200_whenHasAdminTagPermission() throws Exception {
mockMvc.perform(delete("/api/tags/" + UUID.randomUUID()))
.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,124 @@
package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.dto.DocumentUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DocumentServiceTest {
@Mock DocumentRepository documentRepository;
@Mock PersonService personService;
@Mock FileService fileService;
@Mock TagService tagService;
@InjectMocks DocumentService documentService;
// ─── getDocumentById ──────────────────────────────────────────────────────
@Test
void getDocumentById_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(documentRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> documentService.getDocumentById(id))
.isInstanceOf(DomainException.class)
.hasMessageContaining(id.toString());
}
@Test
void getDocumentById_returnsDocument_whenFound() {
UUID id = UUID.randomUUID();
Document doc = Document.builder().id(id).title("Test").build();
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
assertThat(documentService.getDocumentById(id)).isEqualTo(doc);
}
// ─── updateDocument ───────────────────────────────────────────────────────
@Test
void updateDocument_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(documentRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> documentService.updateDocument(id, new DocumentUpdateDTO(), null))
.isInstanceOf(DomainException.class);
}
// ─── deleteTagCascading ───────────────────────────────────────────────────
@Test
void deleteTagCascading_removesTagFromAllDocumentsAndDeletesTag() {
UUID tagId = UUID.randomUUID();
Tag tag = Tag.builder().id(tagId).name("Familie").build();
Document doc = Document.builder()
.id(UUID.randomUUID())
.tags(new HashSet<>(Set.of(tag)))
.build();
when(documentRepository.findByTags_Id(tagId)).thenReturn(List.of(doc));
when(documentRepository.save(any())).thenReturn(doc);
documentService.deleteTagCascading(tagId);
assertThat(doc.getTags()).isEmpty();
verify(tagService).delete(tagId);
}
@Test
void deleteTagCascading_worksWhenNoDocumentsHaveTag() {
UUID tagId = UUID.randomUUID();
when(documentRepository.findByTags_Id(tagId)).thenReturn(List.of());
documentService.deleteTagCascading(tagId);
verify(documentRepository, never()).save(any());
verify(tagService).delete(tagId);
}
// ─── createPlaceholder ────────────────────────────────────────────────────
@Test
void createPlaceholder_returnsExisting_whenAlreadyExists() {
String filename = "scan001.pdf";
Document existing = Document.builder().id(UUID.randomUUID()).originalFilename(filename).build();
when(documentRepository.existsByOriginalFilename(filename)).thenReturn(true);
when(documentRepository.findByOriginalFilename(filename)).thenReturn(Optional.of(existing));
Document result = documentService.createPlaceholder(filename);
assertThat(result).isEqualTo(existing);
verify(documentRepository, never()).save(any());
}
@Test
void createPlaceholder_createsNew_whenNotExists() {
String filename = "scan002.pdf";
Document saved = Document.builder().id(UUID.randomUUID()).originalFilename(filename).build();
when(documentRepository.existsByOriginalFilename(filename)).thenReturn(false);
when(documentRepository.save(any())).thenReturn(saved);
Document result = documentService.createPlaceholder(filename);
assertThat(result).isEqualTo(saved);
verify(documentRepository).save(any());
}
}

View File

@@ -0,0 +1,140 @@
package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PersonServiceTest {
@Mock PersonRepository personRepository;
@InjectMocks PersonService personService;
// ─── getById ─────────────────────────────────────────────────────────────
@Test
void getById_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(personRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> personService.getById(id))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
@Test
void getById_returnsPerson_whenFound() {
UUID id = UUID.randomUUID();
Person person = Person.builder().id(id).firstName("Hans").lastName("Müller").build();
when(personRepository.findById(id)).thenReturn(Optional.of(person));
assertThat(personService.getById(id)).isEqualTo(person);
}
// ─── findOrCreateByAlias ─────────────────────────────────────────────────
@Test
void findOrCreateByAlias_returnsExisting_whenAliasFound() {
String alias = "Walter de Gruyter";
Person existing = Person.builder().id(UUID.randomUUID()).alias(alias).build();
when(personRepository.findByAliasIgnoreCase(alias)).thenReturn(Optional.of(existing));
Person result = personService.findOrCreateByAlias(alias);
assertThat(result).isEqualTo(existing);
verify(personRepository, never()).save(any());
}
@Test
void findOrCreateByAlias_createsNew_whenAliasNotFound() {
String alias = "Clara Cram";
Person saved = Person.builder().id(UUID.randomUUID()).alias(alias).firstName("Clara").lastName("Cram").build();
when(personRepository.findByAliasIgnoreCase(alias)).thenReturn(Optional.empty());
when(personRepository.save(any())).thenReturn(saved);
Person result = personService.findOrCreateByAlias(alias);
assertThat(result).isEqualTo(saved);
verify(personRepository).save(any());
}
@Test
void findOrCreateByAlias_trimsInput() {
String alias = " Clara Cram ";
Person saved = Person.builder().id(UUID.randomUUID()).alias("Clara Cram").build();
when(personRepository.findByAliasIgnoreCase("Clara Cram")).thenReturn(Optional.of(saved));
personService.findOrCreateByAlias(alias);
verify(personRepository).findByAliasIgnoreCase("Clara Cram");
}
// ─── mergePersons ─────────────────────────────────────────────────────────
@Test
void mergePersons_throwsBadRequest_whenSourceEqualsTarget() {
UUID id = UUID.randomUUID();
assertThatThrownBy(() -> personService.mergePersons(id, id))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(400);
}
@Test
void mergePersons_throwsNotFound_whenSourceMissing() {
UUID sourceId = UUID.randomUUID();
UUID targetId = UUID.randomUUID();
when(personRepository.findById(sourceId)).thenReturn(Optional.empty());
assertThatThrownBy(() -> personService.mergePersons(sourceId, targetId))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
@Test
void mergePersons_throwsNotFound_whenTargetMissing() {
UUID sourceId = UUID.randomUUID();
UUID targetId = UUID.randomUUID();
Person source = Person.builder().id(sourceId).firstName("Anna").lastName("Alt").build();
when(personRepository.findById(sourceId)).thenReturn(Optional.of(source));
when(personRepository.findById(targetId)).thenReturn(Optional.empty());
assertThatThrownBy(() -> personService.mergePersons(sourceId, targetId))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
@Test
void mergePersons_reassignsDocumentsAndDeletesSource() {
UUID sourceId = UUID.randomUUID();
UUID targetId = UUID.randomUUID();
Person source = Person.builder().id(sourceId).firstName("Anna").lastName("Alt").build();
Person target = Person.builder().id(targetId).firstName("Anna").lastName("Neu").build();
when(personRepository.findById(sourceId)).thenReturn(Optional.of(source));
when(personRepository.findById(targetId)).thenReturn(Optional.of(target));
personService.mergePersons(sourceId, targetId);
verify(personRepository).reassignSender(sourceId, targetId);
verify(personRepository).insertMissingReceiverReference(sourceId, targetId);
verify(personRepository).deleteReceiverReferences(sourceId);
verify(personRepository).deleteById(sourceId);
}
}

View File

@@ -0,0 +1,131 @@
package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TagServiceTest {
@Mock TagRepository tagRepository;
@InjectMocks TagService tagService;
// ─── getById ─────────────────────────────────────────────────────────────
@Test
void getById_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(tagRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> tagService.getById(id))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
@Test
void getById_returnsTag_whenFound() {
UUID id = UUID.randomUUID();
Tag tag = Tag.builder().id(id).name("Familie").build();
when(tagRepository.findById(id)).thenReturn(Optional.of(tag));
assertThat(tagService.getById(id)).isEqualTo(tag);
}
// ─── findOrCreate ─────────────────────────────────────────────────────────
@Test
void findOrCreate_returnsExisting_whenNameFound() {
Tag existing = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
when(tagRepository.findByNameIgnoreCase("Familie")).thenReturn(Optional.of(existing));
Tag result = tagService.findOrCreate("Familie");
assertThat(result).isEqualTo(existing);
verify(tagRepository, never()).save(any());
}
@Test
void findOrCreate_createsNew_whenNameNotFound() {
Tag saved = Tag.builder().id(UUID.randomUUID()).name("Krieg").build();
when(tagRepository.findByNameIgnoreCase("Krieg")).thenReturn(Optional.empty());
when(tagRepository.save(any())).thenReturn(saved);
Tag result = tagService.findOrCreate("Krieg");
assertThat(result).isEqualTo(saved);
verify(tagRepository).save(any());
}
@Test
void findOrCreate_trimsWhitespaceBeforeLookup() {
Tag existing = Tag.builder().id(UUID.randomUUID()).name("Urlaub").build();
when(tagRepository.findByNameIgnoreCase("Urlaub")).thenReturn(Optional.of(existing));
tagService.findOrCreate(" Urlaub ");
verify(tagRepository).findByNameIgnoreCase("Urlaub");
}
// ─── update ───────────────────────────────────────────────────────────────
@Test
void update_savesNewName() {
UUID id = UUID.randomUUID();
Tag tag = Tag.builder().id(id).name("Old").build();
when(tagRepository.findById(id)).thenReturn(Optional.of(tag));
when(tagRepository.save(tag)).thenAnswer(inv -> inv.getArgument(0));
Tag result = tagService.update(id, "New");
assertThat(result.getName()).isEqualTo("New");
}
@Test
void update_throwsNotFound_whenTagMissing() {
UUID id = UUID.randomUUID();
when(tagRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> tagService.update(id, "New"))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
// ─── delete ───────────────────────────────────────────────────────────────
@Test
void delete_callsRepositoryDelete() {
UUID id = UUID.randomUUID();
Tag tag = Tag.builder().id(id).name("ToDelete").build();
when(tagRepository.findById(id)).thenReturn(Optional.of(tag));
tagService.delete(id);
verify(tagRepository).delete(tag);
}
@Test
void delete_throwsNotFound_whenTagMissing() {
UUID id = UUID.randomUUID();
when(tagRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> tagService.delete(id))
.isInstanceOf(ResponseStatusException.class)
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
.isEqualTo(404);
}
}

View File

@@ -0,0 +1,122 @@
package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.dto.CreateUserRequest;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.model.AppUser;
import org.raddatz.familienarchiv.repository.AppUserRepository;
import org.raddatz.familienarchiv.repository.UserGroupRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock AppUserRepository userRepository;
@Mock UserGroupRepository groupRepository;
@Mock PasswordEncoder passwordEncoder;
@InjectMocks UserService userService;
// ─── findByUsername ───────────────────────────────────────────────────────
@Test
void findByUsername_throwsNotFound_whenMissing() {
when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.findByUsername("ghost"))
.isInstanceOf(DomainException.class);
}
@Test
void findByUsername_returnsUser_whenFound() {
AppUser user = AppUser.builder().id(UUID.randomUUID()).username("admin").build();
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
assertThat(userService.findByUsername("admin")).isEqualTo(user);
}
// ─── deleteUser ───────────────────────────────────────────────────────────
@Test
void deleteUser_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(userRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.deleteUser(id))
.isInstanceOf(DomainException.class);
}
@Test
void deleteUser_deletesUser_whenFound() {
UUID id = UUID.randomUUID();
AppUser user = AppUser.builder().id(id).username("gast").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user));
userService.deleteUser(id);
verify(userRepository).delete(user);
}
// ─── createUserOrUpdate ───────────────────────────────────────────────────
@Test
void createUserOrUpdate_createsNewUser_whenNotExists() {
CreateUserRequest req = new CreateUserRequest();
req.setUsername("newuser");
req.setEmail("new@example.com");
req.setInitialPassword("secret");
req.setGroupIds(List.of());
when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty());
when(passwordEncoder.encode("secret")).thenReturn("encoded");
AppUser saved = AppUser.builder().id(UUID.randomUUID()).username("newuser").build();
when(userRepository.save(any())).thenReturn(saved);
AppUser result = userService.createUserOrUpdate(req);
assertThat(result).isEqualTo(saved);
verify(userRepository).save(any());
}
@Test
void createUserOrUpdate_updatesExistingUser_whenFound() {
CreateUserRequest req = new CreateUserRequest();
req.setUsername("existing");
req.setEmail("existing@example.com");
req.setInitialPassword("newpass");
req.setGroupIds(List.of());
AppUser existing = AppUser.builder().id(UUID.randomUUID()).username("existing").build();
when(userRepository.findByUsername("existing")).thenReturn(Optional.of(existing));
when(passwordEncoder.encode(any())).thenReturn("encoded");
when(userRepository.save(any())).thenReturn(existing);
userService.createUserOrUpdate(req);
// save called once with the updated existing user (no new user created)
verify(userRepository, times(1)).save(existing);
}
// ─── getGroupById ─────────────────────────────────────────────────────────
@Test
void getGroupById_throwsNotFound_whenMissing() {
UUID id = UUID.randomUUID();
when(groupRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.getGroupById(id))
.isInstanceOf(DomainException.class);
}
}