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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user