test(person): pin query count-parity and delete FK-detach ordering
Add countByFilter parity coverage for the query (LIKE) path so the shared FILTER_WHERE slice and count can't drift, and an integration test proving deletePerson detaches a person referenced as both sender and receiver before delete — the documents survive (sender nulled, receiver link removed) with no FK orphan. Refs #667 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -648,6 +648,21 @@ class PersonRepositoryTest {
|
|||||||
assertThat(count).isEqualTo(1);
|
assertThat(count).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void countByFilter_query_matchesSliceSize() {
|
||||||
|
// The whole point of the shared FILTER_WHERE is that the slice and the count can never
|
||||||
|
// drift. Pin the query (LIKE) path explicitly: countByFilter must equal the slice size
|
||||||
|
// so a future edit to one query's LIKE clause is caught.
|
||||||
|
seedDirectoryFixture();
|
||||||
|
|
||||||
|
List<PersonSummaryDTO> slice = personRepository.findByFilter(
|
||||||
|
null, null, null, null, false, "Verlag", 50, 0);
|
||||||
|
long count = personRepository.countByFilter(null, null, null, null, false, "Verlag");
|
||||||
|
|
||||||
|
assertThat(count).isEqualTo(slice.size());
|
||||||
|
assertThat(count).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findByFilter_projectsDocumentCount() {
|
void findByFilter_projectsDocumentCount() {
|
||||||
seedDirectoryFixture();
|
seedDirectoryFixture();
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package org.raddatz.familienarchiv.person;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||||
|
import org.raddatz.familienarchiv.document.Document;
|
||||||
|
import org.raddatz.familienarchiv.document.DocumentRepository;
|
||||||
|
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||||
import org.raddatz.familienarchiv.person.Person;
|
import org.raddatz.familienarchiv.person.Person;
|
||||||
import org.raddatz.familienarchiv.person.PersonType;
|
import org.raddatz.familienarchiv.person.PersonType;
|
||||||
import org.raddatz.familienarchiv.person.PersonRepository;
|
import org.raddatz.familienarchiv.person.PersonRepository;
|
||||||
@@ -13,6 +16,11 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||||
@@ -24,6 +32,9 @@ class PersonServiceIntegrationTest {
|
|||||||
@MockitoBean S3Client s3Client;
|
@MockitoBean S3Client s3Client;
|
||||||
@Autowired PersonService personService;
|
@Autowired PersonService personService;
|
||||||
@Autowired PersonRepository personRepository;
|
@Autowired PersonRepository personRepository;
|
||||||
|
@Autowired DocumentRepository documentRepository;
|
||||||
|
|
||||||
|
@PersistenceContext EntityManager entityManager;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findOrCreateByAlias_skipReturnsNull_noRecordCreated() {
|
void findOrCreateByAlias_skipReturnsNull_noRecordCreated() {
|
||||||
@@ -112,4 +123,48 @@ class PersonServiceIntegrationTest {
|
|||||||
|
|
||||||
assertThat(personRepository.findById(target.getId())).isEmpty();
|
assertThat(personRepository.findById(target.getId())).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deletePerson_detachesSentAndReceivedReferences_beforeDelete_noOrphan() {
|
||||||
|
// A person referenced as BOTH a document sender and a document receiver must delete
|
||||||
|
// cleanly: deletePerson nulls the sender_id FK and removes the receiver join row first
|
||||||
|
// (reassignSenderToNull → deleteReceiverReferences → deleteById), so no FK orphan and
|
||||||
|
// the documents themselves survive.
|
||||||
|
Person target = personRepository.save(Person.builder()
|
||||||
|
.firstName("Weg").lastName("Person").provisional(true).build());
|
||||||
|
Person bystander = personRepository.save(Person.builder()
|
||||||
|
.firstName("Bleibt").lastName("Hier").build());
|
||||||
|
|
||||||
|
Document sent = documentRepository.save(Document.builder()
|
||||||
|
.title("Sent letter").originalFilename("sent.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED).sender(target).build());
|
||||||
|
Document received = documentRepository.save(Document.builder()
|
||||||
|
.title("Received letter").originalFilename("received.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED).sender(bystander)
|
||||||
|
.receivers(new java.util.HashSet<>(Set.of(target))).build());
|
||||||
|
|
||||||
|
// Persist the fixture and detach everything so the native @Modifying deletes operate on
|
||||||
|
// the database directly without the persistence context holding stale references that
|
||||||
|
// would re-flush a now-deleted person as a transient association.
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
personService.deletePerson(target.getId());
|
||||||
|
|
||||||
|
// Native @Modifying queries bypass the persistence context — clear it so the asserting
|
||||||
|
// reads observe the post-delete database state, not stale managed entities.
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
assertThat(personRepository.findById(target.getId())).isEmpty();
|
||||||
|
|
||||||
|
Document reloadedSent = documentRepository.findById(sent.getId()).orElseThrow();
|
||||||
|
assertThat(reloadedSent.getSender()).isNull();
|
||||||
|
|
||||||
|
Document reloadedReceived = documentRepository.findById(received.getId()).orElseThrow();
|
||||||
|
assertThat(reloadedReceived.getReceivers())
|
||||||
|
.noneMatch(p -> p.getId().equals(target.getId()));
|
||||||
|
// The other person and the documents themselves survive the delete.
|
||||||
|
assertThat(personRepository.findById(bystander.getId())).isPresent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user