feat(service): add alias CRUD methods to PersonService
getAliases (sorted by sort_order), addAlias (auto-incrementing sort_order), removeAlias (with IDOR protection verifying alias belongs to the given person). All TDD with 7 new unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,14 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.raddatz.familienarchiv.dto.PersonNameAliasDTO;
|
||||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||||
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
|
import org.raddatz.familienarchiv.model.PersonNameAlias;
|
||||||
|
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
||||||
import org.raddatz.familienarchiv.repository.PersonRepository;
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -22,6 +25,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
public class PersonService {
|
public class PersonService {
|
||||||
|
|
||||||
private final PersonRepository personRepository;
|
private final PersonRepository personRepository;
|
||||||
|
private final PersonNameAliasRepository aliasRepository;
|
||||||
|
|
||||||
public List<PersonSummaryDTO> findAll(String q) {
|
public List<PersonSummaryDTO> findAll(String q) {
|
||||||
if (q == null) {
|
if (q == null) {
|
||||||
@@ -137,4 +141,35 @@ public class PersonService {
|
|||||||
|
|
||||||
personRepository.deleteById(sourceId);
|
personRepository.deleteById(sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Alias management ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<PersonNameAlias> getAliases(UUID personId) {
|
||||||
|
getById(personId);
|
||||||
|
return aliasRepository.findByPersonIdOrderBySortOrderAscCreatedAtAsc(personId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PersonNameAlias addAlias(UUID personId, PersonNameAliasDTO dto) {
|
||||||
|
Person person = getById(personId);
|
||||||
|
int nextSortOrder = aliasRepository.findMaxSortOrder(personId) + 1;
|
||||||
|
PersonNameAlias alias = PersonNameAlias.builder()
|
||||||
|
.person(person)
|
||||||
|
.lastName(dto.lastName())
|
||||||
|
.firstName(dto.firstName())
|
||||||
|
.type(dto.type())
|
||||||
|
.sortOrder(nextSortOrder)
|
||||||
|
.build();
|
||||||
|
return aliasRepository.save(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeAlias(UUID personId, UUID aliasId) {
|
||||||
|
PersonNameAlias alias = aliasRepository.findById(aliasId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(ErrorCode.ALIAS_NOT_FOUND, "Alias not found: " + aliasId));
|
||||||
|
if (!alias.getPerson().getId().equals(personId)) {
|
||||||
|
throw DomainException.forbidden("Alias does not belong to this person");
|
||||||
|
}
|
||||||
|
aliasRepository.delete(alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.raddatz.familienarchiv.dto.PersonNameAliasDTO;
|
||||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||||
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
|
import org.raddatz.familienarchiv.model.PersonNameAlias;
|
||||||
|
import org.raddatz.familienarchiv.model.PersonNameAliasType;
|
||||||
|
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
||||||
import org.raddatz.familienarchiv.repository.PersonRepository;
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ import static org.mockito.Mockito.*;
|
|||||||
class PersonServiceTest {
|
class PersonServiceTest {
|
||||||
|
|
||||||
@Mock PersonRepository personRepository;
|
@Mock PersonRepository personRepository;
|
||||||
|
@Mock PersonNameAliasRepository aliasRepository;
|
||||||
@InjectMocks PersonService personService;
|
@InjectMocks PersonService personService;
|
||||||
|
|
||||||
// ─── getById ─────────────────────────────────────────────────────────────
|
// ─── getById ─────────────────────────────────────────────────────────────
|
||||||
@@ -436,4 +441,99 @@ class PersonServiceTest {
|
|||||||
verify(personRepository).deleteReceiverReferences(sourceId);
|
verify(personRepository).deleteReceiverReferences(sourceId);
|
||||||
verify(personRepository).deleteById(sourceId);
|
verify(personRepository).deleteById(sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── getAliases ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAliases_returnsSortedAliases() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
when(personRepository.findById(personId)).thenReturn(Optional.of(
|
||||||
|
Person.builder().id(personId).firstName("Clara").lastName("Cram").build()));
|
||||||
|
List<PersonNameAlias> aliases = List.of(
|
||||||
|
PersonNameAlias.builder().id(UUID.randomUUID()).lastName("de Gruyter").type(PersonNameAliasType.BIRTH).sortOrder(0).build());
|
||||||
|
when(aliasRepository.findByPersonIdOrderBySortOrderAscCreatedAtAsc(personId)).thenReturn(aliases);
|
||||||
|
|
||||||
|
List<PersonNameAlias> result = personService.getAliases(personId);
|
||||||
|
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).getLastName()).isEqualTo("de Gruyter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getAliases_throwsNotFound_whenPersonMissing() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
when(personRepository.findById(personId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> personService.getAliases(personId))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── addAlias ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAlias_savesWithAutoIncrementedSortOrder() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
Person person = Person.builder().id(personId).firstName("Clara").lastName("Cram").build();
|
||||||
|
when(personRepository.findById(personId)).thenReturn(Optional.of(person));
|
||||||
|
when(aliasRepository.findMaxSortOrder(personId)).thenReturn(2);
|
||||||
|
when(aliasRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
PersonNameAliasDTO dto = new PersonNameAliasDTO("de Gruyter", null, PersonNameAliasType.BIRTH);
|
||||||
|
PersonNameAlias result = personService.addAlias(personId, dto);
|
||||||
|
|
||||||
|
assertThat(result.getSortOrder()).isEqualTo(3);
|
||||||
|
assertThat(result.getLastName()).isEqualTo("de Gruyter");
|
||||||
|
assertThat(result.getPerson()).isEqualTo(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAlias_throwsNotFound_whenPersonMissing() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
when(personRepository.findById(personId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
PersonNameAliasDTO dto = new PersonNameAliasDTO("de Gruyter", null, PersonNameAliasType.BIRTH);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> personService.addAlias(personId, dto))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── removeAlias ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeAlias_deletesAlias_whenBelongsToPerson() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
UUID aliasId = UUID.randomUUID();
|
||||||
|
Person person = Person.builder().id(personId).firstName("Clara").lastName("Cram").build();
|
||||||
|
PersonNameAlias alias = PersonNameAlias.builder().id(aliasId).person(person).lastName("de Gruyter").build();
|
||||||
|
when(aliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));
|
||||||
|
|
||||||
|
personService.removeAlias(personId, aliasId);
|
||||||
|
|
||||||
|
verify(aliasRepository).delete(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeAlias_throwsNotFound_whenAliasMissing() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
UUID aliasId = UUID.randomUUID();
|
||||||
|
when(aliasRepository.findById(aliasId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> personService.removeAlias(personId, aliasId))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson() {
|
||||||
|
UUID personId = UUID.randomUUID();
|
||||||
|
UUID otherPersonId = UUID.randomUUID();
|
||||||
|
UUID aliasId = UUID.randomUUID();
|
||||||
|
Person otherPerson = Person.builder().id(otherPersonId).firstName("Other").lastName("Person").build();
|
||||||
|
PersonNameAlias alias = PersonNameAlias.builder().id(aliasId).person(otherPerson).lastName("de Gruyter").build();
|
||||||
|
when(aliasRepository.findById(aliasId)).thenReturn(Optional.of(alias));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> personService.removeAlias(personId, aliasId))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting(e -> ((DomainException) e).getStatus().value())
|
||||||
|
.isEqualTo(403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user