feat(transcription): person @mention sidecar + rename propagation (PR-A backend, #362) #366

Merged
marcel merged 40 commits from feat/person-mentions-issue-362-backend into main 2026-04-28 23:54:40 +02:00
Showing only changes of commit 48492330a7 - Show all commits

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);