feat(stammbaum): RelationshipService + family_member toggle (TDD)
- Add PersonService.setFamilyMember (write, @Transactional) and findAllFamilyMembers; PersonRepository gains the findByFamilyMemberTrueOrderBy projection. - RelationshipService orchestrates PersonService + the inference service; never reaches into PersonRepository directly. addRelationship guards self-relationship, year range, circular PARENT_OF (Nora B2), and DataIntegrityViolation→DUPLICATE_RELATIONSHIP. deleteRelationship enforces ownership from either side (Nora B1). - Extend RelationshipDTO with personDisplayName + birth/death year so the frontend can render rows from either viewpoint. - 8 unit tests, written against a stub (red), then green: FORBIDDEN delete, CIRCULAR add, DUPLICATE add, self-relationship, year range, happy-path persistence, ownership-from-object, RELATIONSHIP_NOT_FOUND. Full backend suite: 1399/1399 green. Refs #358. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
|||||||
|
package org.raddatz.familienarchiv.relationship;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.CreateRelationshipRequest;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipDTO;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipWithPersonDTO;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.NetworkDTO;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.PersonNodeDTO;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.RelationshipDTO;
|
||||||
|
import org.raddatz.familienarchiv.service.PersonService;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the {@code person_relationships} table and the family_member flag.
|
||||||
|
* Always orchestrates {@link PersonService} for cross-domain access — never
|
||||||
|
* touches {@link org.raddatz.familienarchiv.repository.PersonRepository}.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RelationshipService {
|
||||||
|
|
||||||
|
private final PersonRelationshipRepository relationshipRepository;
|
||||||
|
private final PersonService personService;
|
||||||
|
private final RelationshipInferenceService inferenceService;
|
||||||
|
|
||||||
|
public List<RelationshipDTO> getRelationships(UUID personId) {
|
||||||
|
personService.getById(personId);
|
||||||
|
List<PersonRelationship> rels = relationshipRepository.findAllByPersonIdOrRelatedPersonId(personId);
|
||||||
|
return rels.stream().map(RelationshipService::toDTO).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InferredRelationshipWithPersonDTO> getInferredRelationships(UUID personId) {
|
||||||
|
personService.getById(personId);
|
||||||
|
return inferenceService.findAllFor(personId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<InferredRelationshipDTO> getRelationshipBetween(UUID a, UUID b) {
|
||||||
|
personService.getById(a);
|
||||||
|
personService.getById(b);
|
||||||
|
return inferenceService.infer(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkDTO getFamilyNetwork() {
|
||||||
|
// Two queries: 1 for nodes (family members), 1 for edges (family-graph types).
|
||||||
|
List<Person> familyMembers = personService.findAllFamilyMembers();
|
||||||
|
Set<UUID> familyIds = new HashSet<>(familyMembers.size());
|
||||||
|
List<PersonNodeDTO> nodes = new ArrayList<>(familyMembers.size());
|
||||||
|
for (Person p : familyMembers) {
|
||||||
|
familyIds.add(p.getId());
|
||||||
|
nodes.add(new PersonNodeDTO(
|
||||||
|
p.getId(), p.getDisplayName(), p.getBirthYear(), p.getDeathYear(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PersonRelationship> familyEdges = relationshipRepository.findAllByRelationTypeIn(
|
||||||
|
List.of(RelationType.PARENT_OF, RelationType.SPOUSE_OF, RelationType.SIBLING_OF));
|
||||||
|
|
||||||
|
List<RelationshipDTO> edges = new ArrayList<>();
|
||||||
|
for (PersonRelationship r : familyEdges) {
|
||||||
|
UUID p = r.getPerson().getId();
|
||||||
|
UUID rp = r.getRelatedPerson().getId();
|
||||||
|
if (familyIds.contains(p) && familyIds.contains(rp)) {
|
||||||
|
edges.add(toDTO(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new NetworkDTO(nodes, edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RelationshipDTO addRelationship(UUID personId, CreateRelationshipRequest dto) {
|
||||||
|
if (personId.equals(dto.relatedPersonId())) {
|
||||||
|
throw DomainException.badRequest(
|
||||||
|
ErrorCode.VALIDATION_ERROR, "Cannot relate a person to themselves");
|
||||||
|
}
|
||||||
|
Person person = personService.getById(personId);
|
||||||
|
Person relatedPerson = personService.getById(dto.relatedPersonId());
|
||||||
|
|
||||||
|
RelationType type = parseType(dto.relationType());
|
||||||
|
validateYears(dto.fromYear(), dto.toYear());
|
||||||
|
|
||||||
|
if (type == RelationType.PARENT_OF
|
||||||
|
&& relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
|
relatedPerson.getId(), personId, RelationType.PARENT_OF)) {
|
||||||
|
throw DomainException.conflict(
|
||||||
|
ErrorCode.CIRCULAR_RELATIONSHIP,
|
||||||
|
"Reverse PARENT_OF already exists between " + personId + " and " + relatedPerson.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonRelationship rel = PersonRelationship.builder()
|
||||||
|
.person(person)
|
||||||
|
.relatedPerson(relatedPerson)
|
||||||
|
.relationType(type)
|
||||||
|
.fromYear(dto.fromYear())
|
||||||
|
.toYear(dto.toYear())
|
||||||
|
.notes(blankToNull(dto.notes()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return toDTO(relationshipRepository.save(rel));
|
||||||
|
} catch (DataIntegrityViolationException e) {
|
||||||
|
throw DomainException.conflict(
|
||||||
|
ErrorCode.DUPLICATE_RELATIONSHIP,
|
||||||
|
"Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + type + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteRelationship(UUID personId, UUID relId) {
|
||||||
|
PersonRelationship rel = relationshipRepository.findById(relId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(
|
||||||
|
ErrorCode.RELATIONSHIP_NOT_FOUND, "Relationship not found: " + relId));
|
||||||
|
|
||||||
|
UUID storageSubject = rel.getPerson().getId();
|
||||||
|
UUID storageObject = rel.getRelatedPerson().getId();
|
||||||
|
if (!personId.equals(storageSubject) && !personId.equals(storageObject)) {
|
||||||
|
throw DomainException.forbidden(
|
||||||
|
"Relationship " + relId + " does not belong to person " + personId);
|
||||||
|
}
|
||||||
|
relationshipRepository.delete(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Person setFamilyMember(UUID personId, boolean familyMember) {
|
||||||
|
return personService.setFamilyMember(personId, familyMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? null : s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RelationType parseType(String typeName) {
|
||||||
|
if (typeName == null) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "relationType is required");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return RelationType.valueOf(typeName);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw DomainException.badRequest(
|
||||||
|
ErrorCode.VALIDATION_ERROR, "Invalid relationType: " + typeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateYears(Integer fromYear, Integer toYear) {
|
||||||
|
if (fromYear != null && toYear != null && toYear < fromYear) {
|
||||||
|
throw DomainException.badRequest(
|
||||||
|
ErrorCode.VALIDATION_ERROR, "toYear must not be before fromYear");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RelationshipDTO toDTO(PersonRelationship r) {
|
||||||
|
Person p = r.getPerson();
|
||||||
|
Person rp = r.getRelatedPerson();
|
||||||
|
return new RelationshipDTO(
|
||||||
|
r.getId(),
|
||||||
|
p.getId(),
|
||||||
|
rp.getId(),
|
||||||
|
p.getDisplayName(),
|
||||||
|
p.getBirthYear(),
|
||||||
|
p.getDeathYear(),
|
||||||
|
rp.getDisplayName(),
|
||||||
|
rp.getBirthYear(),
|
||||||
|
rp.getDeathYear(),
|
||||||
|
r.getRelationType(),
|
||||||
|
r.getFromYear(),
|
||||||
|
r.getToYear(),
|
||||||
|
r.getNotes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,22 @@ import org.raddatz.familienarchiv.relationship.RelationType;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape for a stored relationship row. Carries enough context for the
|
* Wire shape for one stored relationship row. Both sides include name + years
|
||||||
* frontend to render a chip (type), a name (relatedPersonDisplayName), a year
|
* so the frontend can render the row from either perspective (e.g. on the
|
||||||
* range, and a delete action (id).
|
* subject's page the row reads "Elternteil von [related]"; on the object's
|
||||||
|
* page it reads "Kind von [person]").
|
||||||
|
*
|
||||||
|
* <p>Storage truth: {@code personId} is the {@code person_id} column,
|
||||||
|
* {@code relatedPersonId} is the {@code related_person_id} column. The
|
||||||
|
* frontend determines orientation by comparing against the viewpoint.
|
||||||
*/
|
*/
|
||||||
public record RelationshipDTO(
|
public record RelationshipDTO(
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID personId,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID personId,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID relatedPersonId,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID relatedPersonId,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String personDisplayName,
|
||||||
|
Integer personBirthYear,
|
||||||
|
Integer personDeathYear,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String relatedPersonDisplayName,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String relatedPersonDisplayName,
|
||||||
Integer relatedPersonBirthYear,
|
Integer relatedPersonBirthYear,
|
||||||
Integer relatedPersonDeathYear,
|
Integer relatedPersonDeathYear,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
|
|||||||
// Hilfsmethode: Alle sortiert laden (für den leeren Status)
|
// Hilfsmethode: Alle sortiert laden (für den leeren Status)
|
||||||
List<Person> findAllByOrderByLastNameAscFirstNameAsc();
|
List<Person> findAllByOrderByLastNameAscFirstNameAsc();
|
||||||
|
|
||||||
|
// Stammbaum-Knoten: alle Personen mit family_member = true.
|
||||||
|
List<Person> findByFamilyMemberTrueOrderByLastNameAscFirstNameAsc();
|
||||||
|
|
||||||
// Lookup by full alias string, used during ODS mass import
|
// Lookup by full alias string, used during ODS mass import
|
||||||
Optional<Person> findByAliasIgnoreCase(String alias);
|
Optional<Person> findByAliasIgnoreCase(String alias);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,17 @@ public class PersonService {
|
|||||||
return personRepository.findAllById(ids);
|
return personRepository.findAllById(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Person> findAllFamilyMembers() {
|
||||||
|
return personRepository.findByFamilyMemberTrueOrderByLastNameAscFirstNameAsc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Person setFamilyMember(UUID personId, boolean familyMember) {
|
||||||
|
Person person = getById(personId);
|
||||||
|
person.setFamilyMember(familyMember);
|
||||||
|
return personRepository.save(person);
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<Person> findByName(String firstName, String lastName) {
|
public Optional<Person> findByName(String firstName, String lastName) {
|
||||||
return personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(firstName, lastName);
|
return personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(firstName, lastName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package org.raddatz.familienarchiv.relationship;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
|
import org.raddatz.familienarchiv.relationship.dto.CreateRelationshipRequest;
|
||||||
|
import org.raddatz.familienarchiv.service.PersonService;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
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.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Felix Brandt — TDD red for RelationshipService domain rules.
|
||||||
|
*
|
||||||
|
* <p>Required by the plan (Nora blockers 1 + 2):
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code deleteRelationship_throws_FORBIDDEN_when_rel_belongs_to_different_person}</li>
|
||||||
|
* <li>{@code addRelationship_throws_CIRCULAR_when_reverse_PARENT_OF_exists}</li>
|
||||||
|
* </ul>
|
||||||
|
* Plus: duplicate constraint, self-relationship, year-range, happy-path persistence,
|
||||||
|
* and ownership permitted from either side.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RelationshipServiceTest {
|
||||||
|
|
||||||
|
@Mock PersonRelationshipRepository relationshipRepository;
|
||||||
|
@Mock PersonService personService;
|
||||||
|
@Mock RelationshipInferenceService inferenceService;
|
||||||
|
@InjectMocks RelationshipService service;
|
||||||
|
|
||||||
|
Person alice;
|
||||||
|
Person bob;
|
||||||
|
Person charlie;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void seed() {
|
||||||
|
alice = person("Alice");
|
||||||
|
bob = person("Bob");
|
||||||
|
charlie = person("Charlie");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Nora blocker 1 ---
|
||||||
|
@Test
|
||||||
|
void deleteRelationship_throws_FORBIDDEN_when_rel_belongs_to_different_person() {
|
||||||
|
UUID relId = UUID.randomUUID();
|
||||||
|
PersonRelationship rel = parentOf(alice, bob, relId);
|
||||||
|
when(relationshipRepository.findById(relId)).thenReturn(Optional.of(rel));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.deleteRelationship(charlie.getId(), relId))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.FORBIDDEN);
|
||||||
|
verify(relationshipRepository, never()).delete(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Nora blocker 2 ---
|
||||||
|
@Test
|
||||||
|
void addRelationship_throws_CIRCULAR_when_reverse_PARENT_OF_exists() {
|
||||||
|
// alice PARENT_OF bob already exists. Now we try to add bob PARENT_OF alice.
|
||||||
|
when(personService.getById(bob.getId())).thenReturn(bob);
|
||||||
|
when(personService.getById(alice.getId())).thenReturn(alice);
|
||||||
|
when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
|
alice.getId(), bob.getId(), RelationType.PARENT_OF)).thenReturn(true);
|
||||||
|
|
||||||
|
var dto = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null);
|
||||||
|
assertThatThrownBy(() -> service.addRelationship(bob.getId(), dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.CIRCULAR_RELATIONSHIP);
|
||||||
|
verify(relationshipRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRelationship_throws_DUPLICATE_when_db_constraint_violated() {
|
||||||
|
when(personService.getById(alice.getId())).thenReturn(alice);
|
||||||
|
when(personService.getById(bob.getId())).thenReturn(bob);
|
||||||
|
when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
|
bob.getId(), alice.getId(), RelationType.PARENT_OF)).thenReturn(false);
|
||||||
|
when(relationshipRepository.save(any())).thenThrow(new DataIntegrityViolationException("unique_rel"));
|
||||||
|
|
||||||
|
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null);
|
||||||
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.DUPLICATE_RELATIONSHIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRelationship_throws_BAD_REQUEST_when_self_relationship() {
|
||||||
|
var dto = new CreateRelationshipRequest(alice.getId(), "FRIEND", null, null, null);
|
||||||
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR);
|
||||||
|
verify(relationshipRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRelationship_throws_BAD_REQUEST_when_to_year_before_from_year() {
|
||||||
|
when(personService.getById(alice.getId())).thenReturn(alice);
|
||||||
|
when(personService.getById(bob.getId())).thenReturn(bob);
|
||||||
|
var dto = new CreateRelationshipRequest(bob.getId(), "FRIEND", 1950, 1940, null);
|
||||||
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR);
|
||||||
|
verify(relationshipRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRelationship_persists_with_storage_truth() {
|
||||||
|
when(personService.getById(alice.getId())).thenReturn(alice);
|
||||||
|
when(personService.getById(bob.getId())).thenReturn(bob);
|
||||||
|
when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
|
bob.getId(), alice.getId(), RelationType.PARENT_OF)).thenReturn(false);
|
||||||
|
when(relationshipRepository.save(any())).thenAnswer(inv -> {
|
||||||
|
PersonRelationship r = inv.getArgument(0);
|
||||||
|
r.setId(UUID.randomUUID());
|
||||||
|
r.setCreatedAt(Instant.now());
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, "first born");
|
||||||
|
var result = service.addRelationship(alice.getId(), dto);
|
||||||
|
|
||||||
|
assertThat(result.personId()).isEqualTo(alice.getId());
|
||||||
|
assertThat(result.relatedPersonId()).isEqualTo(bob.getId());
|
||||||
|
assertThat(result.relationType()).isEqualTo(RelationType.PARENT_OF);
|
||||||
|
assertThat(result.fromYear()).isEqualTo(1900);
|
||||||
|
assertThat(result.notes()).isEqualTo("first born");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteRelationship_succeeds_when_viewpoint_is_object() {
|
||||||
|
UUID relId = UUID.randomUUID();
|
||||||
|
PersonRelationship rel = parentOf(alice, bob, relId);
|
||||||
|
when(relationshipRepository.findById(relId)).thenReturn(Optional.of(rel));
|
||||||
|
|
||||||
|
// Bob is the storage related_person; deleting from his viewpoint should work.
|
||||||
|
service.deleteRelationship(bob.getId(), relId);
|
||||||
|
verify(relationshipRepository).delete(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteRelationship_throws_NOT_FOUND_when_relId_unknown() {
|
||||||
|
UUID relId = UUID.randomUUID();
|
||||||
|
when(relationshipRepository.findById(relId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.deleteRelationship(alice.getId(), relId))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting("code")
|
||||||
|
.isEqualTo(ErrorCode.RELATIONSHIP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
private static Person person(String name) {
|
||||||
|
return Person.builder().id(UUID.randomUUID()).lastName(name).familyMember(true).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PersonRelationship parentOf(Person parent, Person child, UUID id) {
|
||||||
|
return PersonRelationship.builder()
|
||||||
|
.id(id)
|
||||||
|
.person(parent)
|
||||||
|
.relatedPerson(child)
|
||||||
|
.relationType(RelationType.PARENT_OF)
|
||||||
|
.createdAt(Instant.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user