test(person): optimistic-lock test exercises real listener saveAllAndFlush path

Sara #3 / Felix #5 (PR #366 review). The previous version stubbed
eventPublisher.publishEvent to throw, which proved the catch-and-translate
syntax but skipped the listener entirely. The test could not have detected
a regression where the listener swallowed the exception or re-wrapped it
with a non-OptimisticLocking type.

Replace with a real PersonMentionPropagationListener instance backed by a
mocked TranscriptionBlockRepository whose saveAllAndFlush throws
ObjectOptimisticLockingFailureException (the actual Spring exception
Hibernate raises). The publisher mock routes the event to the real
listener via doAnswer so the call chain is the production one:
PersonService.updatePerson → publishEvent → listener.onPersonDisplayNameChanged
→ blockRepository.saveAllAndFlush throws → exception bubbles through the
synchronous event dispatcher → PersonService catches → DomainException.

Refs #362 #366

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-28 21:36:54 +02:00
parent d924d9059c
commit 48492330a7

View File

@@ -13,15 +13,19 @@ import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent;
import org.raddatz.familienarchiv.model.PersonMention;
import org.raddatz.familienarchiv.model.PersonNameAlias;
import org.raddatz.familienarchiv.model.PersonNameAliasType;
import org.raddatz.familienarchiv.model.PersonType;
import org.raddatz.familienarchiv.model.TranscriptionBlock;
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -300,16 +304,38 @@ class PersonServiceTest {
}
@Test
void updatePerson_throwsConflict_whenListenerSignalsOptimisticLockFailure() {
void updatePerson_throwsConflict_whenBlockSaveAllAndFlushHitsOptimisticLock() {
// Wire a real PersonMentionPropagationListener with a mocked block repository
// that throws on saveAllAndFlush. The publisher mock routes events to the
// listener so the catch path traverses the same call chain as production:
// PersonService → publishEvent → listener → saveAllAndFlush throws → catch.
UUID id = UUID.randomUUID();
Person existing = Person.builder()
.id(id).firstName("Auguste").lastName("Raddatz")
.personType(PersonType.PERSON).build();
TranscriptionBlock referencingBlock = TranscriptionBlock.builder()
.id(UUID.randomUUID()).documentId(UUID.randomUUID()).annotationId(UUID.randomUUID())
.text("Brief von @Auguste Raddatz").sortOrder(0)
.mentionedPersons(new ArrayList<>(List.of(new PersonMention(id, "Auguste Raddatz"))))
.build();
TranscriptionBlockRepository blockRepo = mock(TranscriptionBlockRepository.class);
when(blockRepo.findByMentionedPersons_PersonId(id))
.thenReturn(List.of(referencingBlock));
when(blockRepo.saveAllAndFlush(any()))
.thenThrow(new ObjectOptimisticLockingFailureException(
TranscriptionBlock.class, referencingBlock.getId()));
PersonMentionPropagationListener realListener =
new PersonMentionPropagationListener(blockRepo);
when(personRepository.findById(id)).thenReturn(Optional.of(existing));
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
doThrow(new OptimisticLockingFailureException("simulated concurrent block save"))
.when(eventPublisher).publishEvent(any(PersonDisplayNameChangedEvent.class));
doAnswer(inv -> {
realListener.onPersonDisplayNameChanged(inv.getArgument(0));
return null;
}).when(eventPublisher).publishEvent(any(PersonDisplayNameChangedEvent.class));
PersonUpdateDTO dto = new PersonUpdateDTO();
dto.setPersonType(PersonType.PERSON);