|
|
|
|
@@ -0,0 +1,167 @@
|
|
|
|
|
package org.raddatz.familienarchiv.relationship;
|
|
|
|
|
|
|
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
|
|
|
|
import org.raddatz.familienarchiv.config.FlywayConfig;
|
|
|
|
|
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.NetworkDTO;
|
|
|
|
|
import org.raddatz.familienarchiv.relationship.dto.RelationshipDTO;
|
|
|
|
|
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
|
|
|
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
|
|
|
|
import org.raddatz.familienarchiv.service.PersonService;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
|
|
|
|
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
|
|
|
|
import org.springframework.context.annotation.Import;
|
|
|
|
|
|
|
|
|
|
import jakarta.persistence.EntityManager;
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sara blocker 1 — service+DB integration over the family-network constraints.
|
|
|
|
|
* Hits the real Postgres so unique_rel, ON DELETE CASCADE, and the partial
|
|
|
|
|
* sibling index actually fire.
|
|
|
|
|
*/
|
|
|
|
|
@DataJpaTest
|
|
|
|
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
|
|
|
|
@Import({
|
|
|
|
|
PostgresContainerConfig.class,
|
|
|
|
|
FlywayConfig.class,
|
|
|
|
|
RelationshipService.class,
|
|
|
|
|
RelationshipInferenceService.class,
|
|
|
|
|
PersonService.class
|
|
|
|
|
})
|
|
|
|
|
class RelationshipServiceIntegrationTest {
|
|
|
|
|
|
|
|
|
|
@Autowired RelationshipService relationshipService;
|
|
|
|
|
@Autowired PersonRepository personRepository;
|
|
|
|
|
@Autowired PersonRelationshipRepository relationshipRepository;
|
|
|
|
|
// PersonService → PersonNameAliasRepository; @DataJpaTest auto-loads it.
|
|
|
|
|
@Autowired PersonNameAliasRepository aliasRepository;
|
|
|
|
|
@Autowired EntityManager entityManager;
|
|
|
|
|
|
|
|
|
|
Person alice;
|
|
|
|
|
Person bob;
|
|
|
|
|
Person charlie;
|
|
|
|
|
|
|
|
|
|
@BeforeEach
|
|
|
|
|
void seed() {
|
|
|
|
|
relationshipRepository.deleteAll();
|
|
|
|
|
aliasRepository.deleteAll();
|
|
|
|
|
personRepository.deleteAll();
|
|
|
|
|
alice = personRepository.save(Person.builder().firstName("Alice").lastName("Müller").familyMember(true).build());
|
|
|
|
|
bob = personRepository.save(Person.builder().firstName("Bob").lastName("Müller").familyMember(true).build());
|
|
|
|
|
charlie = personRepository.save(Person.builder().firstName("Charlie").lastName("Schmidt").familyMember(false).build());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void addRelationship_stores_and_is_readable() {
|
|
|
|
|
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, null);
|
|
|
|
|
|
|
|
|
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(), dto);
|
|
|
|
|
|
|
|
|
|
assertThat(created.id()).isNotNull();
|
|
|
|
|
assertThat(created.personId()).isEqualTo(alice.getId());
|
|
|
|
|
assertThat(created.relatedPersonId()).isEqualTo(bob.getId());
|
|
|
|
|
|
|
|
|
|
List<RelationshipDTO> rels = relationshipService.getRelationships(alice.getId());
|
|
|
|
|
assertThat(rels).hasSize(1);
|
|
|
|
|
assertThat(rels.get(0).relationType()).isEqualTo(RelationType.PARENT_OF);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void addRelationship_throws_409_when_duplicate() {
|
|
|
|
|
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null);
|
|
|
|
|
relationshipService.addRelationship(alice.getId(), dto);
|
|
|
|
|
|
|
|
|
|
assertThatThrownBy(() -> relationshipService.addRelationship(alice.getId(), dto))
|
|
|
|
|
.isInstanceOf(DomainException.class)
|
|
|
|
|
.extracting("code")
|
|
|
|
|
.isEqualTo(ErrorCode.DUPLICATE_RELATIONSHIP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void addRelationship_throws_409_when_circular_parent() {
|
|
|
|
|
// alice PARENT_OF bob; now try bob PARENT_OF alice → must be rejected.
|
|
|
|
|
relationshipService.addRelationship(alice.getId(),
|
|
|
|
|
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
|
|
|
|
|
|
|
|
|
var reverse = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null);
|
|
|
|
|
assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
|
|
|
|
|
.isInstanceOf(DomainException.class)
|
|
|
|
|
.extracting("code")
|
|
|
|
|
.isEqualTo(ErrorCode.CIRCULAR_RELATIONSHIP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void deleteRelationship_throws_403_when_rel_belongs_to_different_person() {
|
|
|
|
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
|
|
|
|
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
|
|
|
|
|
|
|
|
|
// Charlie is unrelated to this row.
|
|
|
|
|
assertThatThrownBy(() -> relationshipService.deleteRelationship(charlie.getId(), created.id()))
|
|
|
|
|
.isInstanceOf(DomainException.class)
|
|
|
|
|
.extracting("code")
|
|
|
|
|
.isEqualTo(ErrorCode.FORBIDDEN);
|
|
|
|
|
|
|
|
|
|
// The row is still there.
|
|
|
|
|
assertThat(relationshipRepository.findById(created.id())).isPresent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void deleteRelationship_succeeds_for_symmetric_type_from_either_side() {
|
|
|
|
|
// alice SPOUSE_OF bob. Bob deletes from his side.
|
|
|
|
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
|
|
|
|
new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null));
|
|
|
|
|
|
|
|
|
|
relationshipService.deleteRelationship(bob.getId(), created.id());
|
|
|
|
|
|
|
|
|
|
assertThat(relationshipRepository.findById(created.id())).isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void setFamilyMember_true_makes_person_appear_in_network() {
|
|
|
|
|
// charlie starts with familyMember = false. Add a PARENT_OF edge alice→charlie
|
|
|
|
|
// so the edge exists, then flip charlie's flag and verify he appears in nodes.
|
|
|
|
|
relationshipService.addRelationship(alice.getId(),
|
|
|
|
|
new CreateRelationshipRequest(charlie.getId(), "PARENT_OF", null, null, null));
|
|
|
|
|
|
|
|
|
|
NetworkDTO before = relationshipService.getFamilyNetwork();
|
|
|
|
|
assertThat(before.nodes()).extracting("id").doesNotContain(charlie.getId());
|
|
|
|
|
|
|
|
|
|
relationshipService.setFamilyMember(charlie.getId(), true);
|
|
|
|
|
|
|
|
|
|
NetworkDTO after = relationshipService.getFamilyNetwork();
|
|
|
|
|
assertThat(after.nodes()).extracting("id").contains(charlie.getId());
|
|
|
|
|
assertThat(after.edges())
|
|
|
|
|
.anyMatch(e -> e.personId().equals(alice.getId()) && e.relatedPersonId().equals(charlie.getId()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void delete_person_cascades_to_relationships() {
|
|
|
|
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
|
|
|
|
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
|
|
|
|
UUID relId = created.id();
|
|
|
|
|
assertThat(relationshipRepository.findById(relId)).isPresent();
|
|
|
|
|
|
|
|
|
|
// Detach managed entities so deleteById's cascade isn't fought by the
|
|
|
|
|
// persistence context (the rel row still references bob in memory).
|
|
|
|
|
entityManager.flush();
|
|
|
|
|
entityManager.clear();
|
|
|
|
|
|
|
|
|
|
// Delete bob (the relatedPerson) — DB CASCADE must remove the row.
|
|
|
|
|
personRepository.deleteById(bob.getId());
|
|
|
|
|
personRepository.flush();
|
|
|
|
|
|
|
|
|
|
assertThat(relationshipRepository.findById(relId)).isEmpty();
|
|
|
|
|
}
|
|
|
|
|
}
|