feat(geschichte): implement JourneyItemService — append, updateNote, delete, reorder (35 unit tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,242 @@
|
|||||||
|
package org.raddatz.familienarchiv.geschichte.journeyitem;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.openapitools.jackson.nullable.JsonNullable;
|
||||||
|
import org.raddatz.familienarchiv.audit.AuditKind;
|
||||||
|
import org.raddatz.familienarchiv.audit.AuditService;
|
||||||
|
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||||
|
import org.raddatz.familienarchiv.document.Document;
|
||||||
|
import org.raddatz.familienarchiv.document.DocumentService;
|
||||||
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.Geschichte;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.GeschichteRepository;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.GeschichteType;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.DocumentSummary;
|
||||||
|
import org.raddatz.familienarchiv.person.Person;
|
||||||
|
import org.raddatz.familienarchiv.user.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.user.UserService;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class JourneyItemService {
|
||||||
|
|
||||||
|
static final int MAX_ITEMS = 100;
|
||||||
|
static final int POSITION_STEP = 10;
|
||||||
|
static final int MAX_NOTE_LENGTH = 5000;
|
||||||
|
|
||||||
|
private final JourneyItemRepository journeyItemRepository;
|
||||||
|
private final GeschichteRepository geschichteRepository;
|
||||||
|
private final DocumentService documentService;
|
||||||
|
private final AuditService auditService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public JourneyItemView append(UUID geschichteId, JourneyItemCreateDTO dto) {
|
||||||
|
Geschichte g = geschichteRepository.findById(geschichteId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(ErrorCode.GESCHICHTE_NOT_FOUND,
|
||||||
|
"Journey not found: " + geschichteId));
|
||||||
|
|
||||||
|
if (g.getType() != GeschichteType.JOURNEY) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Geschichte is not a JOURNEY — cannot append items");
|
||||||
|
}
|
||||||
|
|
||||||
|
long count = journeyItemRepository.countByGeschichteId(geschichteId);
|
||||||
|
if (count >= MAX_ITEMS) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Journey already has the maximum of " + MAX_ITEMS + " items");
|
||||||
|
}
|
||||||
|
|
||||||
|
String note = normalizeNote(dto.getNote());
|
||||||
|
|
||||||
|
if (dto.getDocumentId() == null && note == null) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"At least one of documentId or note must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note != null && note.length() > MAX_NOTE_LENGTH) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Note exceeds maximum length of " + MAX_NOTE_LENGTH + " characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Document doc = null;
|
||||||
|
if (dto.getDocumentId() != null) {
|
||||||
|
doc = documentService.getSummaryById(dto.getDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextPosition = journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)
|
||||||
|
.map(max -> max + POSITION_STEP)
|
||||||
|
.orElse(POSITION_STEP);
|
||||||
|
|
||||||
|
JourneyItem item = JourneyItem.builder()
|
||||||
|
.geschichte(g)
|
||||||
|
.position(nextPosition)
|
||||||
|
.document(doc)
|
||||||
|
.note(note)
|
||||||
|
.build();
|
||||||
|
JourneyItem saved = journeyItemRepository.save(item);
|
||||||
|
|
||||||
|
UUID actorId = currentUser().getId();
|
||||||
|
auditService.logAfterCommit(AuditKind.JOURNEY_ITEM_ADDED, actorId, null,
|
||||||
|
Map.of("geschichteId", geschichteId, "itemId", saved.getId()));
|
||||||
|
|
||||||
|
return toView(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public JourneyItemView updateNote(UUID geschichteId, UUID itemId, JourneyItemUpdateDTO dto) {
|
||||||
|
JourneyItem item = journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(ErrorCode.JOURNEY_ITEM_NOT_FOUND,
|
||||||
|
"Journey item not found: " + itemId));
|
||||||
|
|
||||||
|
JsonNullable<String> noteField = dto.getNote();
|
||||||
|
if (!noteField.isPresent()) {
|
||||||
|
return toView(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
String note = normalizeNote(noteField.get());
|
||||||
|
|
||||||
|
if (note != null && note.length() > MAX_NOTE_LENGTH) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Note exceeds maximum length of " + MAX_NOTE_LENGTH + " characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note == null && item.getDocumentId() == null) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Cannot clear note on an item that has no linked document");
|
||||||
|
}
|
||||||
|
|
||||||
|
item.setNote(note);
|
||||||
|
JourneyItem saved = journeyItemRepository.save(item);
|
||||||
|
return toView(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(UUID geschichteId, UUID itemId) {
|
||||||
|
JourneyItem item = journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)
|
||||||
|
.orElseThrow(() -> DomainException.notFound(ErrorCode.JOURNEY_ITEM_NOT_FOUND,
|
||||||
|
"Journey item not found: " + itemId));
|
||||||
|
|
||||||
|
journeyItemRepository.delete(item);
|
||||||
|
|
||||||
|
UUID actorId = currentUser().getId();
|
||||||
|
auditService.logAfterCommit(AuditKind.JOURNEY_ITEM_REMOVED, actorId, null,
|
||||||
|
Map.of("geschichteId", geschichteId, "itemId", itemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<JourneyItemView> reorder(UUID geschichteId, JourneyReorderDTO dto) {
|
||||||
|
Set<UUID> existingIds = journeyItemRepository.findIdsByGeschichteId(geschichteId);
|
||||||
|
List<UUID> requestedIds = dto.getItemIds() != null ? dto.getItemIds() : List.of();
|
||||||
|
|
||||||
|
if (!existingIds.equals(new HashSet<>(requestedIds))) {
|
||||||
|
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Requested item IDs do not match the journey's existing items");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<JourneyItem> items = journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId);
|
||||||
|
Map<UUID, JourneyItem> itemMap = new HashMap<>();
|
||||||
|
for (JourneyItem item : items) {
|
||||||
|
itemMap.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<JourneyItem> reordered = new ArrayList<>(requestedIds.size());
|
||||||
|
for (int i = 0; i < requestedIds.size(); i++) {
|
||||||
|
JourneyItem item = itemMap.get(requestedIds.get(i));
|
||||||
|
item.setPosition((i + 1) * POSITION_STEP);
|
||||||
|
reordered.add(journeyItemRepository.save(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID actorId = currentUser().getId();
|
||||||
|
auditService.logAfterCommit(AuditKind.JOURNEY_ITEMS_REORDERED, actorId, null,
|
||||||
|
Map.of("geschichteId", geschichteId, "itemCount", reordered.size()));
|
||||||
|
|
||||||
|
return reordered.stream().map(this::toView).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JourneyItemView> getItems(UUID geschichteId) {
|
||||||
|
return journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId)
|
||||||
|
.stream().map(this::toView).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DocumentSummary toSummary(Document doc) {
|
||||||
|
String senderName = buildSenderName(doc);
|
||||||
|
Set<Person> receivers = doc.getReceivers();
|
||||||
|
String receiverName = buildCanonicalReceiverName(receivers);
|
||||||
|
|
||||||
|
return new DocumentSummary(
|
||||||
|
doc.getId(),
|
||||||
|
doc.getTitle(),
|
||||||
|
doc.getDocumentDate(),
|
||||||
|
doc.getMetaDateEnd(),
|
||||||
|
doc.getMetaDatePrecision() != null ? doc.getMetaDatePrecision() : DatePrecision.UNKNOWN,
|
||||||
|
senderName,
|
||||||
|
receiverName,
|
||||||
|
receivers != null ? receivers.size() : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JourneyItemView toView(JourneyItem item) {
|
||||||
|
DocumentSummary docSummary = null;
|
||||||
|
if (item.getDocumentId() != null) {
|
||||||
|
Document doc = documentService.getSummaryById(item.getDocumentId());
|
||||||
|
docSummary = toSummary(doc);
|
||||||
|
}
|
||||||
|
return new JourneyItemView(item.getId(), item.getPosition(), docSummary, item.getNote());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildSenderName(Document doc) {
|
||||||
|
Person sender = doc.getSender();
|
||||||
|
if (sender != null) {
|
||||||
|
String name = join(sender.getFirstName(), sender.getLastName());
|
||||||
|
if (!name.isBlank()) return name;
|
||||||
|
}
|
||||||
|
String senderText = doc.getSenderText();
|
||||||
|
return (senderText != null && !senderText.isBlank()) ? senderText : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildCanonicalReceiverName(Set<Person> receivers) {
|
||||||
|
if (receivers == null || receivers.isEmpty()) return null;
|
||||||
|
return receivers.stream()
|
||||||
|
.min(Comparator.comparing(p -> sortKey(p.getLastName()) + " " + sortKey(p.getFirstName())))
|
||||||
|
.map(p -> {
|
||||||
|
String name = join(p.getFirstName(), p.getLastName());
|
||||||
|
return name.isBlank() ? null : name;
|
||||||
|
})
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeNote(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) return null;
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String join(String first, String last) {
|
||||||
|
return ((first != null ? first : "") + " " + (last != null ? last : "")).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sortKey(String s) {
|
||||||
|
return s != null ? s : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser currentUser() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth == null || !auth.isAuthenticated()) {
|
||||||
|
throw DomainException.unauthorized("Authentication required");
|
||||||
|
}
|
||||||
|
return userService.findByEmail(auth.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,629 @@
|
|||||||
|
package org.raddatz.familienarchiv.geschichte.journeyitem;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.openapitools.jackson.nullable.JsonNullable;
|
||||||
|
import org.raddatz.familienarchiv.audit.AuditKind;
|
||||||
|
import org.raddatz.familienarchiv.audit.AuditService;
|
||||||
|
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||||
|
import org.raddatz.familienarchiv.document.Document;
|
||||||
|
import org.raddatz.familienarchiv.document.DocumentService;
|
||||||
|
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||||
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.Geschichte;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.GeschichteRepository;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.GeschichteStatus;
|
||||||
|
import org.raddatz.familienarchiv.geschichte.GeschichteType;
|
||||||
|
import org.raddatz.familienarchiv.person.Person;
|
||||||
|
import org.raddatz.familienarchiv.user.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.user.UserService;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class JourneyItemServiceTest {
|
||||||
|
|
||||||
|
@Mock JourneyItemRepository journeyItemRepository;
|
||||||
|
@Mock GeschichteRepository geschichteRepository;
|
||||||
|
@Mock DocumentService documentService;
|
||||||
|
@Mock AuditService auditService;
|
||||||
|
@Mock UserService userService;
|
||||||
|
|
||||||
|
@InjectMocks JourneyItemService journeyItemService;
|
||||||
|
|
||||||
|
UUID geschichteId = UUID.randomUUID();
|
||||||
|
UUID itemId = UUID.randomUUID();
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID actorId = UUID.randomUUID();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupAuth() {
|
||||||
|
AppUser actor = AppUser.builder().id(actorId).email("test@test.de").build();
|
||||||
|
lenient().when(userService.findByEmail("test@test.de")).thenReturn(actor);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(
|
||||||
|
new UsernamePasswordAuthenticationToken("test@test.de", null,
|
||||||
|
List.of(new SimpleGrantedAuthority("BLOG_WRITE"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── toSummary — name composition ────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_uses_linked_person_firstName_lastName() {
|
||||||
|
Person sender = Person.builder().firstName("Franz").lastName("Raddatz").build();
|
||||||
|
Document doc = makeDoc(docId, sender, List.of(), null, null);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.senderName()).isEqualTo("Franz Raddatz");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_falls_back_to_senderText_when_no_person() {
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), "Familie Müller", null);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.senderName()).isEqualTo("Familie Müller");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_returns_null_senderName_when_neither_person_nor_text() {
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.senderName()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_receiverCount_0_and_null_name_when_no_receiver() {
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.receiverCount()).isEqualTo(0);
|
||||||
|
assertThat(summary.receiverName()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_multi_receiver_returns_first_canonical_name_and_total_count() {
|
||||||
|
Person emma = Person.builder().firstName("Emma").lastName("Raddatz").build();
|
||||||
|
Person anna = Person.builder().firstName("Anna").lastName("Amann").build();
|
||||||
|
Document doc = makeDoc(docId, null, List.of(emma, anna), null, null);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.receiverCount()).isEqualTo(2);
|
||||||
|
assertThat(summary.receiverName()).isEqualTo("Anna Amann"); // alphabetically first by lastName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_datePrecision_SEASON_roundtrips() {
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
doc.setMetaDatePrecision(DatePrecision.SEASON);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.datePrecision()).isEqualTo(DatePrecision.SEASON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toSummary_datePrecision_APPROX_roundtrips() {
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
doc.setMetaDatePrecision(DatePrecision.APPROX);
|
||||||
|
|
||||||
|
var summary = journeyItemService.toSummary(doc);
|
||||||
|
|
||||||
|
assertThat(summary.datePrecision()).isEqualTo(DatePrecision.APPROX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── append ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_to_empty_journey_starts_at_10() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
when(journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)).thenReturn(Optional.empty());
|
||||||
|
JourneyItem saved = savedItem(itemId, journey, 10, null, "Note");
|
||||||
|
when(journeyItemRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.append(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(view.position()).isEqualTo(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_after_reorder_continues_from_max_position() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(2L);
|
||||||
|
when(journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)).thenReturn(Optional.of(40));
|
||||||
|
JourneyItem saved = savedItem(itemId, journey, 50, null, "Note");
|
||||||
|
when(journeyItemRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.append(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(view.position()).isEqualTo(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns400_when_neither_documentId_nor_note() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.hasMessageContaining("documentId or note");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns400_when_note_trims_to_empty_and_no_document() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote(" \n ");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns400_when_note_exceeds_5000_chars() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("x".repeat(5001));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns400_on_non_JOURNEY_type() {
|
||||||
|
Geschichte story = Geschichte.builder()
|
||||||
|
.id(geschichteId)
|
||||||
|
.title("Story")
|
||||||
|
.type(GeschichteType.STORY)
|
||||||
|
.status(GeschichteStatus.DRAFT)
|
||||||
|
.build();
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(story));
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns404_when_documentId_does_not_exist() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
when(documentService.getSummaryById(docId))
|
||||||
|
.thenThrow(DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "not found"));
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setDocumentId(docId);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.DOCUMENT_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_returns400_when_100_items_exist() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(100L);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cap_is_COUNT_based_not_MAX_position_based() {
|
||||||
|
// 99 rows with MAX(position)=2000 should still accept the 100th append
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(99L);
|
||||||
|
when(journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)).thenReturn(Optional.of(2000));
|
||||||
|
JourneyItem saved = savedItem(itemId, journey, 2010, null, "Note");
|
||||||
|
when(journeyItemRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
|
||||||
|
assertThat(journeyItemService.append(geschichteId, dto).position()).isEqualTo(2010);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_audits_JOURNEY_ITEM_ADDED() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteRepository.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(0L);
|
||||||
|
when(journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)).thenReturn(Optional.empty());
|
||||||
|
JourneyItem saved = savedItem(itemId, journey, 10, null, "Note");
|
||||||
|
when(journeyItemRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setNote("Note");
|
||||||
|
journeyItemService.append(geschichteId, dto);
|
||||||
|
|
||||||
|
verify(auditService).logAfterCommit(eq(AuditKind.JOURNEY_ITEM_ADDED), eq(actorId), isNull(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── updateNote ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNote_absent_leaves_note_unchanged() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
JourneyItem item = savedItem(itemId, journey, 10, null, "Original note");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
// note is JsonNullable.undefined() by default
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.updateNote(geschichteId, itemId, dto);
|
||||||
|
|
||||||
|
assertThat(view.note()).isEqualTo("Original note");
|
||||||
|
verify(journeyItemRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNote_null_clears_note_when_document_is_present() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
JourneyItem item = savedItemWithDoc(itemId, journey, 10, doc, "Old note");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
when(documentService.getSummaryById(docId)).thenReturn(doc);
|
||||||
|
JourneyItem saved = savedItemWithDoc(itemId, journey, 10, doc, null);
|
||||||
|
when(journeyItemRepository.save(item)).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of(null));
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.updateNote(geschichteId, itemId, dto);
|
||||||
|
|
||||||
|
assertThat(view.note()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNote_string_sets_note() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
JourneyItem item = savedItem(itemId, journey, 10, null, null);
|
||||||
|
item.setNote(null);
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
JourneyItem saved = savedItem(itemId, journey, 10, null, "New note");
|
||||||
|
when(journeyItemRepository.save(item)).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of("New note"));
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.updateNote(geschichteId, itemId, dto);
|
||||||
|
|
||||||
|
assertThat(view.note()).isEqualTo("New note");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNote_null_returns400_when_item_has_no_document() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
JourneyItem item = savedItem(itemId, journey, 10, null, "Only note — no doc");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of(null));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.updateNote(geschichteId, itemId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateNote_whitespace_only_including_newlines_stored_as_null() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
Document doc = makeDoc(docId, null, List.of(), null, null);
|
||||||
|
JourneyItem item = savedItemWithDoc(itemId, journey, 10, doc, "Old");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
when(documentService.getSummaryById(docId)).thenReturn(doc);
|
||||||
|
JourneyItem saved = savedItemWithDoc(itemId, journey, 10, doc, null);
|
||||||
|
when(journeyItemRepository.save(item)).thenReturn(saved);
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of("\n \n"));
|
||||||
|
|
||||||
|
JourneyItemView view = journeyItemService.updateNote(geschichteId, itemId, dto);
|
||||||
|
|
||||||
|
assertThat(view.note()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patch_returns400_when_note_exceeds_5000_chars() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
JourneyItem item = savedItem(itemId, journey, 10, null, "Old");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of("x".repeat(5001)));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.updateNote(geschichteId, itemId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patch_returns404_when_item_belongs_to_different_journey() {
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
JourneyItemUpdateDTO dto = new JourneyItemUpdateDTO();
|
||||||
|
dto.setNote(JsonNullable.of("text"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.updateNote(geschichteId, itemId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.JOURNEY_ITEM_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── delete ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delete_returns404_when_item_already_deleted() {
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.delete(geschichteId, itemId))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.JOURNEY_ITEM_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delete_no_audit_when_item_not_found() {
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.delete(geschichteId, itemId))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
|
||||||
|
verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delete_audits_JOURNEY_ITEM_REMOVED_when_item_found() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
JourneyItem item = savedItem(itemId, journey, 10, null, "Note");
|
||||||
|
when(journeyItemRepository.findByIdAndGeschichteId(itemId, geschichteId)).thenReturn(Optional.of(item));
|
||||||
|
|
||||||
|
journeyItemService.delete(geschichteId, itemId);
|
||||||
|
|
||||||
|
verify(auditService).logAfterCommit(eq(AuditKind.JOURNEY_ITEM_REMOVED), eq(actorId), isNull(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── reorder ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_returns400_when_itemId_belongs_to_different_journey() {
|
||||||
|
UUID foreignId = UUID.randomUUID();
|
||||||
|
UUID localId = UUID.randomUUID();
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(localId));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of(foreignId));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.reorder(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.satisfies(e -> assertThat(((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.VALIDATION_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_returns400_when_ids_have_extra_items() {
|
||||||
|
UUID id1 = UUID.randomUUID();
|
||||||
|
UUID id2 = UUID.randomUUID();
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(id1));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of(id1, id2));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.reorder(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_returns200_when_empty_on_empty_journey() {
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of());
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of());
|
||||||
|
|
||||||
|
List<JourneyItemView> result = journeyItemService.reorder(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_returns400_when_empty_on_nonempty_journey() {
|
||||||
|
UUID id1 = UUID.randomUUID();
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(id1));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.reorder(geschichteId, dto))
|
||||||
|
.isInstanceOf(DomainException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_returns_items_in_new_order_starting_at_10() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
UUID id1 = UUID.randomUUID();
|
||||||
|
UUID id2 = UUID.randomUUID();
|
||||||
|
JourneyItem item1 = savedItem(id1, journey, 20, null, "A");
|
||||||
|
JourneyItem item2 = savedItem(id2, journey, 10, null, "B");
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(id1, id2));
|
||||||
|
when(journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId)).thenReturn(List.of(item2, item1));
|
||||||
|
when(journeyItemRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of(id1, id2)); // want id1 first
|
||||||
|
|
||||||
|
List<JourneyItemView> views = journeyItemService.reorder(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(views).hasSize(2);
|
||||||
|
assertThat(views.get(0).id()).isEqualTo(id1);
|
||||||
|
assertThat(views.get(0).position()).isEqualTo(10);
|
||||||
|
assertThat(views.get(1).id()).isEqualTo(id2);
|
||||||
|
assertThat(views.get(1).position()).isEqualTo(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_identical_order_returns200() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
UUID id1 = UUID.randomUUID();
|
||||||
|
JourneyItem item1 = savedItem(id1, journey, 10, null, "A");
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(id1));
|
||||||
|
when(journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId)).thenReturn(List.of(item1));
|
||||||
|
when(journeyItemRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of(id1));
|
||||||
|
|
||||||
|
List<JourneyItemView> views = journeyItemService.reorder(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(views).hasSize(1);
|
||||||
|
assertThat(views.get(0).position()).isEqualTo(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_of_grandfathered_over_cap_journey_succeeds() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
// 130-item journey — reorder with all 130 IDs must succeed despite > 100 cap
|
||||||
|
List<UUID> ids = new java.util.ArrayList<>();
|
||||||
|
List<JourneyItem> items = new java.util.ArrayList<>();
|
||||||
|
for (int i = 1; i <= 130; i++) {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
ids.add(id);
|
||||||
|
items.add(savedItem(id, journey, i * 10, null, "item " + i));
|
||||||
|
}
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(new HashSet<>(ids));
|
||||||
|
when(journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId)).thenReturn(items);
|
||||||
|
when(journeyItemRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(ids);
|
||||||
|
|
||||||
|
List<JourneyItemView> views = journeyItemService.reorder(geschichteId, dto);
|
||||||
|
|
||||||
|
assertThat(views).hasSize(130);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorder_audits_JOURNEY_ITEMS_REORDERED() {
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
UUID id1 = UUID.randomUUID();
|
||||||
|
JourneyItem item1 = savedItem(id1, journey, 10, null, "A");
|
||||||
|
when(journeyItemRepository.findIdsByGeschichteId(geschichteId)).thenReturn(Set.of(id1));
|
||||||
|
when(journeyItemRepository.findByGeschichteIdOrderByPosition(geschichteId)).thenReturn(List.of(item1));
|
||||||
|
when(journeyItemRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
JourneyReorderDTO dto = new JourneyReorderDTO();
|
||||||
|
dto.setItemIds(List.of(id1));
|
||||||
|
journeyItemService.reorder(geschichteId, dto);
|
||||||
|
|
||||||
|
verify(auditService).logAfterCommit(eq(AuditKind.JOURNEY_ITEMS_REORDERED), eq(actorId), isNull(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Geschichte journey(UUID id) {
|
||||||
|
return Geschichte.builder()
|
||||||
|
.id(id)
|
||||||
|
.title("Test Journey")
|
||||||
|
.type(GeschichteType.JOURNEY)
|
||||||
|
.status(GeschichteStatus.DRAFT)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JourneyItem savedItem(UUID id, Geschichte g, int position, Document doc, String note) {
|
||||||
|
return JourneyItem.builder()
|
||||||
|
.id(id)
|
||||||
|
.geschichte(g)
|
||||||
|
.position(position)
|
||||||
|
.document(null) // no document entity to avoid LAZY issues in unit tests
|
||||||
|
.note(note)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JourneyItem savedItemWithDoc(UUID id, Geschichte g, int position, Document doc, String note) {
|
||||||
|
JourneyItem item = JourneyItem.builder()
|
||||||
|
.id(id)
|
||||||
|
.geschichte(g)
|
||||||
|
.position(position)
|
||||||
|
.document(doc)
|
||||||
|
.note(note)
|
||||||
|
.build();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document makeDoc(UUID id, Person sender, List<Person> receivers, String senderText, String receiverText) {
|
||||||
|
Document doc = Document.builder()
|
||||||
|
.id(id)
|
||||||
|
.title("Test Doc")
|
||||||
|
.originalFilename("test.pdf")
|
||||||
|
.status(DocumentStatus.UPLOADED)
|
||||||
|
.senderText(senderText)
|
||||||
|
.receiverText(receiverText)
|
||||||
|
.sender(sender)
|
||||||
|
.build();
|
||||||
|
doc.setReceivers(new HashSet<>(receivers));
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user