Refactor domain models to records and move exceptions to sub-package
All checks were successful
CI / backend-test (push) Successful in 1m1s
CI / frontend-test (push) Successful in 26s
CI / frontend-e2e (push) Successful in 1m25s
CI / build-and-publish (push) Has been skipped

- Convert Event and Rsvp from mutable POJOs to Java records
- Move all 8 exception classes to application.service.exception sub-package
- Add ArchUnit rule enforcing domain models must be records

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 20:09:27 +01:00
parent 541017965f
commit d333ab3d39
24 changed files with 296 additions and 417 deletions

View File

@@ -9,8 +9,8 @@ import de.fete.adapter.in.web.model.CreateRsvpResponse;
import de.fete.adapter.in.web.model.GetAttendeesResponse; import de.fete.adapter.in.web.model.GetAttendeesResponse;
import de.fete.adapter.in.web.model.GetEventResponse; import de.fete.adapter.in.web.model.GetEventResponse;
import de.fete.adapter.in.web.model.PatchEventRequest; import de.fete.adapter.in.web.model.PatchEventRequest;
import de.fete.application.service.EventNotFoundException; import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.InvalidTimezoneException; import de.fete.application.service.exception.InvalidTimezoneException;
import de.fete.domain.model.CreateEventCommand; import de.fete.domain.model.CreateEventCommand;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
@@ -78,11 +78,11 @@ public class EventController implements EventsApi {
Event event = createEventUseCase.createEvent(command); Event event = createEventUseCase.createEvent(command);
var response = new CreateEventResponse(); var response = new CreateEventResponse();
response.setEventToken(event.getEventToken().value()); response.setEventToken(event.eventToken().value());
response.setOrganizerToken(event.getOrganizerToken().value()); response.setOrganizerToken(event.organizerToken().value());
response.setTitle(event.getTitle()); response.setTitle(event.title());
response.setDateTime(event.getDateTime()); response.setDateTime(event.dateTime());
response.setTimezone(event.getTimezone().getId()); response.setTimezone(event.timezone().getId());
return ResponseEntity.status(HttpStatus.CREATED).body(response); return ResponseEntity.status(HttpStatus.CREATED).body(response);
} }
@@ -94,16 +94,16 @@ public class EventController implements EventsApi {
.orElseThrow(() -> new EventNotFoundException(eventToken)); .orElseThrow(() -> new EventNotFoundException(eventToken));
var response = new GetEventResponse(); var response = new GetEventResponse();
response.setEventToken(event.getEventToken().value()); response.setEventToken(event.eventToken().value());
response.setTitle(event.getTitle()); response.setTitle(event.title());
response.setDescription(event.getDescription()); response.setDescription(event.description());
response.setDateTime(event.getDateTime()); response.setDateTime(event.dateTime());
response.setTimezone(event.getTimezone().getId()); response.setTimezone(event.timezone().getId());
response.setLocation(event.getLocation()); response.setLocation(event.location());
response.setAttendeeCount( response.setAttendeeCount(
(int) countAttendeesByEventUseCase.countByEvent(evtToken)); (int) countAttendeesByEventUseCase.countByEvent(evtToken));
response.setCancelled(event.isCancelled()); response.setCancelled(event.cancelled());
response.setCancellationReason(event.getCancellationReason()); response.setCancellationReason(event.cancellationReason());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@@ -145,8 +145,8 @@ public class EventController implements EventsApi {
Rsvp rsvp = createRsvpUseCase.createRsvp(evtToken, createRsvpRequest.getName()); Rsvp rsvp = createRsvpUseCase.createRsvp(evtToken, createRsvpRequest.getName());
var response = new CreateRsvpResponse(); var response = new CreateRsvpResponse();
response.setRsvpToken(rsvp.getRsvpToken().value()); response.setRsvpToken(rsvp.rsvpToken().value());
response.setName(rsvp.getName()); response.setName(rsvp.name());
return ResponseEntity.status(HttpStatus.CREATED).body(response); return ResponseEntity.status(HttpStatus.CREATED).body(response);
} }

View File

@@ -1,13 +1,13 @@
package de.fete.adapter.in.web; package de.fete.adapter.in.web;
import de.fete.application.service.EventAlreadyCancelledException; import de.fete.application.service.exception.EventAlreadyCancelledException;
import de.fete.application.service.EventCancelledException; import de.fete.application.service.exception.EventCancelledException;
import de.fete.application.service.EventExpiredException; import de.fete.application.service.exception.EventExpiredException;
import de.fete.application.service.EventNotFoundException; import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.ExpiryDateBeforeEventException; import de.fete.application.service.exception.ExpiryDateBeforeEventException;
import de.fete.application.service.ExpiryDateInPastException; import de.fete.application.service.exception.ExpiryDateInPastException;
import de.fete.application.service.InvalidOrganizerTokenException; import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.application.service.InvalidTimezoneException; import de.fete.application.service.exception.InvalidTimezoneException;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View File

@@ -86,11 +86,11 @@ public class SpaController {
private Map<String, String> buildEventMeta(Event event, String baseUrl) { private Map<String, String> buildEventMeta(Event event, String baseUrl) {
var tags = new LinkedHashMap<String, String>(); var tags = new LinkedHashMap<String, String>();
String title = truncateTitle(event.getTitle()); String title = truncateTitle(event.title());
String description = formatDescription(event); String description = formatDescription(event);
tags.put("og:title", title); tags.put("og:title", title);
tags.put("og:description", description); tags.put("og:description", description);
tags.put("og:url", baseUrl + "/events/" + event.getEventToken().value()); tags.put("og:url", baseUrl + "/events/" + event.eventToken().value());
tags.put("og:type", "website"); tags.put("og:type", "website");
tags.put("og:site_name", GENERIC_TITLE); tags.put("og:site_name", GENERIC_TITLE);
tags.put("og:image", baseUrl + "/og-image.png"); tags.put("og:image", baseUrl + "/og-image.png");
@@ -138,16 +138,16 @@ public class SpaController {
} }
private String formatDescription(Event event) { private String formatDescription(Event event) {
ZonedDateTime zoned = event.getDateTime().atZoneSameInstant(event.getTimezone()); ZonedDateTime zoned = event.dateTime().atZoneSameInstant(event.timezone());
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.append("📅 ").append(zoned.format(DATE_FORMAT)); sb.append("📅 ").append(zoned.format(DATE_FORMAT));
if (event.getLocation() != null && !event.getLocation().isBlank()) { if (event.location() != null && !event.location().isBlank()) {
sb.append(" · 📍 ").append(event.getLocation()); sb.append(" · 📍 ").append(event.location());
} }
if (event.getDescription() != null && !event.getDescription().isBlank()) { if (event.description() != null && !event.description().isBlank()) {
sb.append("").append(event.getDescription()); sb.append("").append(event.description());
} }
String result = sb.toString(); String result = sb.toString();

View File

@@ -38,35 +38,34 @@ public class EventPersistenceAdapter implements EventRepository {
private EventJpaEntity toEntity(Event event) { private EventJpaEntity toEntity(Event event) {
var entity = new EventJpaEntity(); var entity = new EventJpaEntity();
entity.setId(event.getId()); entity.setId(event.id());
entity.setEventToken(event.getEventToken().value()); entity.setEventToken(event.eventToken().value());
entity.setOrganizerToken(event.getOrganizerToken().value()); entity.setOrganizerToken(event.organizerToken().value());
entity.setTitle(event.getTitle()); entity.setTitle(event.title());
entity.setDescription(event.getDescription()); entity.setDescription(event.description());
entity.setDateTime(event.getDateTime()); entity.setDateTime(event.dateTime());
entity.setTimezone(event.getTimezone().getId()); entity.setTimezone(event.timezone().getId());
entity.setLocation(event.getLocation()); entity.setLocation(event.location());
entity.setExpiryDate(event.getExpiryDate()); entity.setExpiryDate(event.expiryDate());
entity.setCreatedAt(event.getCreatedAt()); entity.setCreatedAt(event.createdAt());
entity.setCancelled(event.isCancelled()); entity.setCancelled(event.cancelled());
entity.setCancellationReason(event.getCancellationReason()); entity.setCancellationReason(event.cancellationReason());
return entity; return entity;
} }
private Event toDomain(EventJpaEntity entity) { private Event toDomain(EventJpaEntity entity) {
var event = new Event(); return new Event(
event.setId(entity.getId()); entity.getId(),
event.setEventToken(new EventToken(entity.getEventToken())); new EventToken(entity.getEventToken()),
event.setOrganizerToken(new OrganizerToken(entity.getOrganizerToken())); new OrganizerToken(entity.getOrganizerToken()),
event.setTitle(entity.getTitle()); entity.getTitle(),
event.setDescription(entity.getDescription()); entity.getDescription(),
event.setDateTime(entity.getDateTime()); entity.getDateTime(),
event.setTimezone(ZoneId.of(entity.getTimezone())); ZoneId.of(entity.getTimezone()),
event.setLocation(entity.getLocation()); entity.getLocation(),
event.setExpiryDate(entity.getExpiryDate()); entity.getExpiryDate(),
event.setCreatedAt(entity.getCreatedAt()); entity.getCreatedAt(),
event.setCancelled(entity.isCancelled()); entity.isCancelled(),
event.setCancellationReason(entity.getCancellationReason()); entity.getCancellationReason());
return event;
} }
} }

View File

@@ -43,19 +43,18 @@ public class RsvpPersistenceAdapter implements RsvpRepository {
private RsvpJpaEntity toEntity(Rsvp rsvp) { private RsvpJpaEntity toEntity(Rsvp rsvp) {
var entity = new RsvpJpaEntity(); var entity = new RsvpJpaEntity();
entity.setId(rsvp.getId()); entity.setId(rsvp.id());
entity.setRsvpToken(rsvp.getRsvpToken().value()); entity.setRsvpToken(rsvp.rsvpToken().value());
entity.setEventId(rsvp.getEventId()); entity.setEventId(rsvp.eventId());
entity.setName(rsvp.getName()); entity.setName(rsvp.name());
return entity; return entity;
} }
private Rsvp toDomain(RsvpJpaEntity entity) { private Rsvp toDomain(RsvpJpaEntity entity) {
var rsvp = new Rsvp(); return new Rsvp(
rsvp.setId(entity.getId()); entity.getId(),
rsvp.setRsvpToken(new RsvpToken(entity.getRsvpToken())); new RsvpToken(entity.getRsvpToken()),
rsvp.setEventId(entity.getEventId()); entity.getEventId(),
rsvp.setName(entity.getName()); entity.getName());
return rsvp;
} }
} }

View File

@@ -1,5 +1,8 @@
package de.fete.application.service; package de.fete.application.service;
import de.fete.application.service.exception.EventAlreadyCancelledException;
import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.domain.model.CreateEventCommand; import de.fete.domain.model.CreateEventCommand;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
@@ -34,16 +37,19 @@ public class EventService implements CreateEventUseCase, GetEventUseCase, Update
public Event createEvent(CreateEventCommand command) { public Event createEvent(CreateEventCommand command) {
LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT); LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
var event = new Event(); var event = new Event(
event.setEventToken(EventToken.generate()); null,
event.setOrganizerToken(OrganizerToken.generate()); EventToken.generate(),
event.setTitle(command.title()); OrganizerToken.generate(),
event.setDescription(command.description()); command.title(),
event.setDateTime(command.dateTime()); command.description(),
event.setTimezone(command.timezone()); command.dateTime(),
event.setLocation(command.location()); command.timezone(),
event.setExpiryDate(expiryDate); command.location(),
event.setCreatedAt(OffsetDateTime.now(clock)); expiryDate,
OffsetDateTime.now(clock),
false,
null);
return eventRepository.save(event); return eventRepository.save(event);
} }
@@ -65,16 +71,14 @@ public class EventService implements CreateEventUseCase, GetEventUseCase, Update
Event event = eventRepository.findByEventToken(eventToken) Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value())); .orElseThrow(() -> new EventNotFoundException(eventToken.value()));
if (!event.getOrganizerToken().equals(organizerToken)) { if (!event.organizerToken().equals(organizerToken)) {
throw new InvalidOrganizerTokenException(); throw new InvalidOrganizerTokenException();
} }
if (event.isCancelled()) { if (event.cancelled()) {
throw new EventAlreadyCancelledException(eventToken.value()); throw new EventAlreadyCancelledException(eventToken.value());
} }
event.setCancelled(true); eventRepository.save(event.withCancellation(true, reason));
event.setCancellationReason(reason);
eventRepository.save(event);
} }
} }

View File

@@ -1,5 +1,9 @@
package de.fete.application.service; package de.fete.application.service;
import de.fete.application.service.exception.EventCancelledException;
import de.fete.application.service.exception.EventExpiredException;
import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken; import de.fete.domain.model.OrganizerToken;
@@ -42,18 +46,15 @@ public class RsvpService
Event event = eventRepository.findByEventToken(eventToken) Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value())); .orElseThrow(() -> new EventNotFoundException(eventToken.value()));
if (event.isCancelled()) { if (event.cancelled()) {
throw new EventCancelledException(eventToken.value()); throw new EventCancelledException(eventToken.value());
} }
if (!event.getExpiryDate().isAfter(LocalDate.now(clock))) { if (!event.expiryDate().isAfter(LocalDate.now(clock))) {
throw new EventExpiredException(eventToken.value()); throw new EventExpiredException(eventToken.value());
} }
var rsvp = new Rsvp(); var rsvp = new Rsvp(null, RsvpToken.generate(), event.id(), name.strip());
rsvp.setRsvpToken(RsvpToken.generate());
rsvp.setEventId(event.getId());
rsvp.setName(name.strip());
return rsvpRepository.save(rsvp); return rsvpRepository.save(rsvp);
} }
@@ -63,14 +64,14 @@ public class RsvpService
public void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken) { public void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken) {
eventRepository.findByEventToken(eventToken) eventRepository.findByEventToken(eventToken)
.ifPresent(event -> .ifPresent(event ->
rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken)); rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken));
} }
@Override @Override
public long countByEvent(EventToken eventToken) { public long countByEvent(EventToken eventToken) {
Event event = eventRepository.findByEventToken(eventToken) Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value())); .orElseThrow(() -> new EventNotFoundException(eventToken.value()));
return rsvpRepository.countByEventId(event.getId()); return rsvpRepository.countByEventId(event.id());
} }
@Override @Override
@@ -78,12 +79,12 @@ public class RsvpService
Event event = eventRepository.findByEventToken(eventToken) Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value())); .orElseThrow(() -> new EventNotFoundException(eventToken.value()));
if (!event.getOrganizerToken().equals(organizerToken)) { if (!event.organizerToken().equals(organizerToken)) {
throw new InvalidOrganizerTokenException(); throw new InvalidOrganizerTokenException();
} }
return rsvpRepository.findByEventId(event.getId()).stream() return rsvpRepository.findByEventId(event.id()).stream()
.map(Rsvp::getName) .map(Rsvp::name)
.toList(); .toList();
} }
} }

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
import java.time.LocalDate; import java.time.LocalDate;

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
/** Thrown when an invalid organizer token is provided. */ /** Thrown when an invalid organizer token is provided. */
public class InvalidOrganizerTokenException extends RuntimeException { public class InvalidOrganizerTokenException extends RuntimeException {

View File

@@ -1,4 +1,4 @@
package de.fete.application.service; package de.fete.application.service.exception;
/** Thrown when an invalid IANA timezone ID is provided. */ /** Thrown when an invalid IANA timezone ID is provided. */
public class InvalidTimezoneException extends RuntimeException { public class InvalidTimezoneException extends RuntimeException {

View File

@@ -0,0 +1,4 @@
/**
* Application-layer exceptions thrown by service use case implementations.
*/
package de.fete.application.service.exception;

View File

@@ -5,138 +5,26 @@ import java.time.OffsetDateTime;
import java.time.ZoneId; import java.time.ZoneId;
/** Domain entity representing an event. */ /** Domain entity representing an event. */
public class Event { public record Event(
Long id,
EventToken eventToken,
OrganizerToken organizerToken,
String title,
String description,
OffsetDateTime dateTime,
ZoneId timezone,
String location,
LocalDate expiryDate,
OffsetDateTime createdAt,
boolean cancelled,
String cancellationReason
) {
private Long id; /** Returns a copy of this event with cancellation applied. */
private EventToken eventToken; public Event withCancellation(boolean cancelled, String cancellationReason) {
private OrganizerToken organizerToken; return new Event(
private String title; id, eventToken, organizerToken, title, description,
private String description; dateTime, timezone, location, expiryDate, createdAt,
private OffsetDateTime dateTime; cancelled, cancellationReason);
private ZoneId timezone;
private String location;
private LocalDate expiryDate;
private OffsetDateTime createdAt;
private boolean cancelled;
private String cancellationReason;
/** Returns the internal database ID. */
public Long getId() {
return id;
}
/** Sets the internal database ID. */
public void setId(Long id) {
this.id = id;
}
/** Returns the public event token. */
public EventToken getEventToken() {
return eventToken;
}
/** Sets the public event token. */
public void setEventToken(EventToken eventToken) {
this.eventToken = eventToken;
}
/** Returns the secret organizer token. */
public OrganizerToken getOrganizerToken() {
return organizerToken;
}
/** Sets the secret organizer token. */
public void setOrganizerToken(OrganizerToken organizerToken) {
this.organizerToken = organizerToken;
}
/** Returns the event title. */
public String getTitle() {
return title;
}
/** Sets the event title. */
public void setTitle(String title) {
this.title = title;
}
/** Returns the event description. */
public String getDescription() {
return description;
}
/** Sets the event description. */
public void setDescription(String description) {
this.description = description;
}
/** Returns the event date and time with UTC offset. */
public OffsetDateTime getDateTime() {
return dateTime;
}
/** Sets the event date and time. */
public void setDateTime(OffsetDateTime dateTime) {
this.dateTime = dateTime;
}
/** Returns the IANA timezone. */
public ZoneId getTimezone() {
return timezone;
}
/** Sets the IANA timezone. */
public void setTimezone(ZoneId timezone) {
this.timezone = timezone;
}
/** Returns the event location. */
public String getLocation() {
return location;
}
/** Sets the event location. */
public void setLocation(String location) {
this.location = location;
}
/** Returns the expiry date after which event data is deleted. */
public LocalDate getExpiryDate() {
return expiryDate;
}
/** Sets the expiry date. */
public void setExpiryDate(LocalDate expiryDate) {
this.expiryDate = expiryDate;
}
/** Returns the creation timestamp. */
public OffsetDateTime getCreatedAt() {
return createdAt;
}
/** Sets the creation timestamp. */
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
/** Returns whether the event has been cancelled. */
public boolean isCancelled() {
return cancelled;
}
/** Sets the cancelled flag. */
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
/** Returns the cancellation reason, if any. */
public String getCancellationReason() {
return cancellationReason;
}
/** Sets the cancellation reason. */
public void setCancellationReason(String cancellationReason) {
this.cancellationReason = cancellationReason;
} }
} }

View File

@@ -1,50 +1,9 @@
package de.fete.domain.model; package de.fete.domain.model;
/** Domain entity representing an RSVP. */ /** Domain entity representing an RSVP. */
public class Rsvp { public record Rsvp(
Long id,
private Long id; RsvpToken rsvpToken,
private RsvpToken rsvpToken; Long eventId,
private Long eventId; String name
private String name; ) {}
/** Returns the internal database ID. */
public Long getId() {
return id;
}
/** Sets the internal database ID. */
public void setId(Long id) {
this.id = id;
}
/** Returns the RSVP token. */
public RsvpToken getRsvpToken() {
return rsvpToken;
}
/** Sets the RSVP token. */
public void setRsvpToken(RsvpToken rsvpToken) {
this.rsvpToken = rsvpToken;
}
/** Returns the event ID this RSVP belongs to. */
public Long getEventId() {
return eventId;
}
/** Sets the event ID. */
public void setEventId(Long eventId) {
this.eventId = eventId;
}
/** Returns the guest's display name. */
public String getName() {
return name;
}
/** Sets the guest's display name. */
public void setName(String name) {
this.name = name;
}
}

View File

@@ -4,10 +4,14 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.onionArchitecture; import static com.tngtech.archunit.library.Architectures.onionArchitecture;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class) @AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
class HexagonalArchitectureTest { class HexagonalArchitectureTest {
@@ -65,4 +69,24 @@ class HexagonalArchitectureTest {
static final ArchRule webAdapterMustNotDependOnOutboundPorts = noClasses() static final ArchRule webAdapterMustNotDependOnOutboundPorts = noClasses()
.that().resideInAPackage("de.fete.adapter.in.web..") .that().resideInAPackage("de.fete.adapter.in.web..")
.should().dependOnClassesThat().resideInAPackage("de.fete.domain.port.out.."); .should().dependOnClassesThat().resideInAPackage("de.fete.domain.port.out..");
@ArchTest
static final ArchRule domainModelsMustBeRecords = classes()
.that().resideInAPackage("de.fete.domain.model..")
.and().doNotHaveSimpleName("package-info")
.should(beRecords());
private static ArchCondition<JavaClass> beRecords() {
return new ArchCondition<>("be records") {
@Override
public void check(JavaClass javaClass,
ConditionEvents events) {
boolean isRecord = javaClass.reflect().isRecord();
if (!isRecord) {
events.add(SimpleConditionEvent.violated(javaClass,
javaClass.getFullName() + " is not a record"));
}
}
};
}
} }

View File

@@ -42,7 +42,7 @@ class EventPersistenceAdapterIntegrationTest {
eventRepository.deleteExpired(); eventRepository.deleteExpired();
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent(); assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
} }
@Test @Test
@@ -52,7 +52,7 @@ class EventPersistenceAdapterIntegrationTest {
eventRepository.deleteExpired(); eventRepository.deleteExpired();
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent(); assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
} }
@Test @Test
@@ -66,16 +66,18 @@ class EventPersistenceAdapterIntegrationTest {
} }
private Event buildEvent(String title, LocalDate expiryDate) { private Event buildEvent(String title, LocalDate expiryDate) {
var event = new Event(); return new Event(
event.setEventToken(EventToken.generate()); null,
event.setOrganizerToken(OrganizerToken.generate()); EventToken.generate(),
event.setTitle(title); OrganizerToken.generate(),
event.setDescription("Test description"); title,
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))); "Test description",
event.setTimezone(ZoneId.of("Europe/Berlin")); OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
event.setLocation("Test Location"); ZoneId.of("Europe/Berlin"),
event.setExpiryDate(expiryDate); "Test Location",
event.setCreatedAt(OffsetDateTime.now()); expiryDate,
return event; OffsetDateTime.now(),
false,
null);
} }
} }

View File

@@ -30,8 +30,8 @@ class EventPersistenceAdapterTest {
Event saved = eventRepository.save(event); Event saved = eventRepository.save(event);
assertThat(saved.getId()).isNotNull(); assertThat(saved.id()).isNotNull();
assertThat(saved.getTitle()).isEqualTo("Test Event"); assertThat(saved.title()).isEqualTo("Test Event");
} }
@Test @Test
@@ -39,11 +39,11 @@ class EventPersistenceAdapterTest {
Event event = buildEvent(); Event event = buildEvent();
Event saved = eventRepository.save(event); Event saved = eventRepository.save(event);
Optional<Event> found = eventRepository.findByEventToken(saved.getEventToken()); Optional<Event> found = eventRepository.findByEventToken(saved.eventToken());
assertThat(found).isPresent(); assertThat(found).isPresent();
assertThat(found.get().getTitle()).isEqualTo("Test Event"); assertThat(found.get().title()).isEqualTo("Test Event");
assertThat(found.get().getId()).isEqualTo(saved.getId()); assertThat(found.get().id()).isEqualTo(saved.id());
} }
@Test @Test
@@ -61,42 +61,47 @@ class EventPersistenceAdapterTest {
OffsetDateTime createdAt = OffsetDateTime createdAt =
OffsetDateTime.of(2026, 3, 4, 12, 0, 0, 0, ZoneOffset.UTC); OffsetDateTime.of(2026, 3, 4, 12, 0, 0, 0, ZoneOffset.UTC);
var event = new Event(); var event = new Event(
event.setEventToken(EventToken.generate()); null,
event.setOrganizerToken(OrganizerToken.generate()); EventToken.generate(),
event.setTitle("Full Event"); OrganizerToken.generate(),
event.setDescription("A detailed description"); "Full Event",
event.setDateTime(dateTime); "A detailed description",
event.setTimezone(ZoneId.of("Europe/Berlin")); dateTime,
event.setLocation("Berlin, Germany"); ZoneId.of("Europe/Berlin"),
event.setExpiryDate(expiryDate); "Berlin, Germany",
event.setCreatedAt(createdAt); expiryDate,
createdAt,
false,
null);
Event saved = eventRepository.save(event); Event saved = eventRepository.save(event);
Event found = eventRepository.findByEventToken(saved.getEventToken()).orElseThrow(); Event found = eventRepository.findByEventToken(saved.eventToken()).orElseThrow();
assertThat(found.getEventToken()).isEqualTo(event.getEventToken()); assertThat(found.eventToken()).isEqualTo(event.eventToken());
assertThat(found.getOrganizerToken()).isEqualTo(event.getOrganizerToken()); assertThat(found.organizerToken()).isEqualTo(event.organizerToken());
assertThat(found.getTitle()).isEqualTo("Full Event"); assertThat(found.title()).isEqualTo("Full Event");
assertThat(found.getDescription()).isEqualTo("A detailed description"); assertThat(found.description()).isEqualTo("A detailed description");
assertThat(found.getDateTime().toInstant()).isEqualTo(dateTime.toInstant()); assertThat(found.dateTime().toInstant()).isEqualTo(dateTime.toInstant());
assertThat(found.getTimezone()).isEqualTo(ZoneId.of("Europe/Berlin")); assertThat(found.timezone()).isEqualTo(ZoneId.of("Europe/Berlin"));
assertThat(found.getLocation()).isEqualTo("Berlin, Germany"); assertThat(found.location()).isEqualTo("Berlin, Germany");
assertThat(found.getExpiryDate()).isEqualTo(expiryDate); assertThat(found.expiryDate()).isEqualTo(expiryDate);
assertThat(found.getCreatedAt().toInstant()).isEqualTo(createdAt.toInstant()); assertThat(found.createdAt().toInstant()).isEqualTo(createdAt.toInstant());
} }
private Event buildEvent() { private Event buildEvent() {
var event = new Event(); return new Event(
event.setEventToken(EventToken.generate()); null,
event.setOrganizerToken(OrganizerToken.generate()); EventToken.generate(),
event.setTitle("Test Event"); OrganizerToken.generate(),
event.setDescription("Test description"); "Test Event",
event.setDateTime(OffsetDateTime.now().plusDays(7)); "Test description",
event.setTimezone(ZoneId.of("Europe/Berlin")); OffsetDateTime.now().plusDays(7),
event.setLocation("Somewhere"); ZoneId.of("Europe/Berlin"),
event.setExpiryDate(LocalDate.now().plusDays(30)); "Somewhere",
event.setCreatedAt(OffsetDateTime.now()); LocalDate.now().plusDays(30),
return event; OffsetDateTime.now(),
false,
null);
} }
} }

View File

@@ -7,7 +7,9 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import de.fete.application.service.EventAlreadyCancelledException; import de.fete.application.service.exception.EventAlreadyCancelledException;
import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken; import de.fete.domain.model.OrganizerToken;
@@ -46,10 +48,8 @@ class EventServiceCancelTest {
void cancelEventDelegatesToDomainAndSaves() { void cancelEventDelegatesToDomainAndSaves() {
EventToken eventToken = EventToken.generate(); EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate(); OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event(); var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
event.setEventToken(eventToken); null, false, null);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
when(eventRepository.findByEventToken(eventToken)) when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
@@ -60,18 +60,16 @@ class EventServiceCancelTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class); ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture()); verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue(); assertThat(captor.getValue().cancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isEqualTo("Venue unavailable"); assertThat(captor.getValue().cancellationReason()).isEqualTo("Venue unavailable");
} }
@Test @Test
void cancelEventWithNullReason() { void cancelEventWithNullReason() {
EventToken eventToken = EventToken.generate(); EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate(); OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event(); var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
event.setEventToken(eventToken); null, false, null);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
when(eventRepository.findByEventToken(eventToken)) when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
@@ -82,8 +80,8 @@ class EventServiceCancelTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class); ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture()); verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue(); assertThat(captor.getValue().cancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isNull(); assertThat(captor.getValue().cancellationReason()).isNull();
} }
@Test @Test
@@ -104,9 +102,8 @@ class EventServiceCancelTest {
void cancelEventThrows403WhenWrongOrganizerToken() { void cancelEventThrows403WhenWrongOrganizerToken() {
EventToken eventToken = EventToken.generate(); EventToken eventToken = EventToken.generate();
OrganizerToken correctToken = OrganizerToken.generate(); OrganizerToken correctToken = OrganizerToken.generate();
var event = new Event(); var event = new Event(null, eventToken, correctToken, null, null, null, null, null, null,
event.setEventToken(eventToken); null, false, null);
event.setOrganizerToken(correctToken);
when(eventRepository.findByEventToken(eventToken)) when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
@@ -122,10 +119,8 @@ class EventServiceCancelTest {
void cancelEventThrows409WhenAlreadyCancelled() { void cancelEventThrows409WhenAlreadyCancelled() {
EventToken eventToken = EventToken.generate(); EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate(); OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event(); var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
event.setEventToken(eventToken); null, true, null);
event.setOrganizerToken(organizerToken);
event.setCancelled(true);
when(eventRepository.findByEventToken(eventToken)) when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));

View File

@@ -57,13 +57,13 @@ class EventServiceTest {
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);
assertThat(result.getTitle()).isEqualTo("Birthday Party"); assertThat(result.title()).isEqualTo("Birthday Party");
assertThat(result.getDescription()).isEqualTo("Come celebrate!"); assertThat(result.description()).isEqualTo("Come celebrate!");
assertThat(result.getTimezone()).isEqualTo(ZONE); assertThat(result.timezone()).isEqualTo(ZONE);
assertThat(result.getLocation()).isEqualTo("Berlin"); assertThat(result.location()).isEqualTo("Berlin");
assertThat(result.getEventToken()).isNotNull(); assertThat(result.eventToken()).isNotNull();
assertThat(result.getOrganizerToken()).isNotNull(); assertThat(result.organizerToken()).isNotNull();
assertThat(result.getCreatedAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE)); assertThat(result.createdAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE));
} }
@Test @Test
@@ -80,7 +80,7 @@ class EventServiceTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class); ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository, times(1)).save(captor.capture()); verify(eventRepository, times(1)).save(captor.capture());
assertThat(captor.getValue().getTitle()).isEqualTo("Test"); assertThat(captor.getValue().title()).isEqualTo("Test");
} }
@Test @Test
@@ -96,7 +96,7 @@ class EventServiceTest {
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(eventDate.plusDays(7)); assertThat(result.expiryDate()).isEqualTo(eventDate.plusDays(7));
} }
// --- GetEventUseCase tests (T004) --- // --- GetEventUseCase tests (T004) ---
@@ -104,16 +104,15 @@ class EventServiceTest {
@Test @Test
void getByEventTokenReturnsEvent() { void getByEventTokenReturnsEvent() {
EventToken token = EventToken.generate(); EventToken token = EventToken.generate();
var event = new Event(); var event = new Event(null, token, null, "Found Event", null, null, null, null, null, null,
event.setEventToken(token); false, null);
event.setTitle("Found Event");
when(eventRepository.findByEventToken(token)) when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
Optional<Event> result = eventService.getByEventToken(token); Optional<Event> result = eventService.getByEventToken(token);
assertThat(result).isPresent(); assertThat(result).isPresent();
assertThat(result.get().getTitle()).isEqualTo("Found Event"); assertThat(result.get().title()).isEqualTo("Found Event");
} }
@Test @Test
@@ -142,6 +141,6 @@ class EventServiceTest {
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);
assertThat(result.getTimezone()).isEqualTo(ZoneId.of("America/New_York")); assertThat(result.timezone()).isEqualTo(ZoneId.of("America/New_York"));
} }
} }

View File

@@ -6,6 +6,10 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import de.fete.application.service.exception.EventCancelledException;
import de.fete.application.service.exception.EventExpiredException;
import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken; import de.fete.domain.model.OrganizerToken;
@@ -51,23 +55,23 @@ class RsvpServiceTest {
@Test @Test
void createRsvpSucceedsForActiveEvent() { void createRsvpSucceedsForActiveEvent() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class))) when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0)); .thenAnswer(invocation -> invocation.getArgument(0));
Rsvp result = rsvpService.createRsvp(token, "Max Mustermann"); Rsvp result = rsvpService.createRsvp(token, "Max Mustermann");
assertThat(result.getName()).isEqualTo("Max Mustermann"); assertThat(result.name()).isEqualTo("Max Mustermann");
assertThat(result.getRsvpToken()).isNotNull(); assertThat(result.rsvpToken()).isNotNull();
assertThat(result.getEventId()).isEqualTo(event.getId()); assertThat(result.eventId()).isEqualTo(event.id());
} }
@Test @Test
void createRsvpPersistsViaRepository() { void createRsvpPersistsViaRepository() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class))) when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0)); .thenAnswer(invocation -> invocation.getArgument(0));
@@ -76,8 +80,8 @@ class RsvpServiceTest {
ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class); ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class);
verify(rsvpRepository).save(captor.capture()); verify(rsvpRepository).save(captor.capture());
assertThat(captor.getValue().getName()).isEqualTo("Test Guest"); assertThat(captor.getValue().name()).isEqualTo("Test Guest");
assertThat(captor.getValue().getEventId()).isEqualTo(event.getId()); assertThat(captor.getValue().eventId()).isEqualTo(event.id());
} }
@Test @Test
@@ -91,22 +95,21 @@ class RsvpServiceTest {
@Test @Test
void createRsvpTrimsName() { void createRsvpTrimsName() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class))) when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0)); .thenAnswer(invocation -> invocation.getArgument(0));
Rsvp result = rsvpService.createRsvp(token, " Max "); Rsvp result = rsvpService.createRsvp(token, " Max ");
assertThat(result.getName()).isEqualTo("Max"); assertThat(result.name()).isEqualTo("Max");
} }
@Test @Test
void createRsvpThrowsWhenEventExpired() { void createRsvpThrowsWhenEventExpired() {
var event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.minusDays(1));
event.setExpiryDate(TODAY.minusDays(1)); EventToken token = event.eventToken();
EventToken token = event.getEventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest")) assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
@@ -115,9 +118,8 @@ class RsvpServiceTest {
@Test @Test
void createRsvpThrowsWhenEventExpiresToday() { void createRsvpThrowsWhenEventExpiresToday() {
var event = buildActiveEvent(); Event event = buildActiveEvent(TODAY);
event.setExpiryDate(TODAY); EventToken token = event.eventToken();
EventToken token = event.getEventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest")) assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
@@ -126,12 +128,12 @@ class RsvpServiceTest {
@Test @Test
void getAttendeeNamesReturnsNamesInOrder() { void getAttendeeNamesReturnsNamesInOrder() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
OrganizerToken orgToken = event.getOrganizerToken(); OrganizerToken orgToken = event.organizerToken();
when(eventRepository.findByEventToken(token)) when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
when(rsvpRepository.findByEventId(event.getId())) when(rsvpRepository.findByEventId(event.id()))
.thenReturn(List.of( .thenReturn(List.of(
buildRsvp(1L, "Alice"), buildRsvp(1L, "Alice"),
buildRsvp(2L, "Bob"), buildRsvp(2L, "Bob"),
@@ -144,12 +146,12 @@ class RsvpServiceTest {
@Test @Test
void getAttendeeNamesReturnsEmptyListWhenNoRsvps() { void getAttendeeNamesReturnsEmptyListWhenNoRsvps() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
OrganizerToken orgToken = event.getOrganizerToken(); OrganizerToken orgToken = event.organizerToken();
when(eventRepository.findByEventToken(token)) when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
when(rsvpRepository.findByEventId(event.getId())) when(rsvpRepository.findByEventId(event.id()))
.thenReturn(List.of()); .thenReturn(List.of());
List<String> names = rsvpService.getAttendeeNames(token, orgToken); List<String> names = rsvpService.getAttendeeNames(token, orgToken);
@@ -171,8 +173,8 @@ class RsvpServiceTest {
@Test @Test
void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() { void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
OrganizerToken wrongToken = OrganizerToken.generate(); OrganizerToken wrongToken = OrganizerToken.generate();
when(eventRepository.findByEventToken(token)) when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event)); .thenReturn(Optional.of(event));
@@ -183,38 +185,33 @@ class RsvpServiceTest {
} }
private Rsvp buildRsvp(Long id, String name) { private Rsvp buildRsvp(Long id, String name) {
var rsvp = new Rsvp(); return new Rsvp(id, RsvpToken.generate(), 1L, name);
rsvp.setId(id);
rsvp.setRsvpToken(RsvpToken.generate());
rsvp.setEventId(1L);
rsvp.setName(name);
return rsvp;
} }
@Test @Test
void cancelRsvpDeletesWhenEventAndRsvpExist() { void cancelRsvpDeletesWhenEventAndRsvpExist() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
RsvpToken rsvpToken = RsvpToken.generate(); RsvpToken rsvpToken = RsvpToken.generate();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken)).thenReturn(true); when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(true);
rsvpService.cancelRsvp(token, rsvpToken); rsvpService.cancelRsvp(token, rsvpToken);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken); verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
} }
@Test @Test
void cancelRsvpSucceedsWhenRsvpNotFound() { void cancelRsvpSucceedsWhenRsvpNotFound() {
Event event = buildActiveEvent(); Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.getEventToken(); EventToken token = event.eventToken();
RsvpToken rsvpToken = RsvpToken.generate(); RsvpToken rsvpToken = RsvpToken.generate();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken)).thenReturn(false); when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(false);
rsvpService.cancelRsvp(token, rsvpToken); rsvpService.cancelRsvp(token, rsvpToken);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken); verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
} }
@Test @Test
@@ -226,16 +223,19 @@ class RsvpServiceTest {
rsvpService.cancelRsvp(token, rsvpToken); rsvpService.cancelRsvp(token, rsvpToken);
} }
private Event buildActiveEvent() { private Event buildActiveEvent(LocalDate expiryDate) {
var event = new Event(); return new Event(
event.setId(1L); 1L,
event.setEventToken(EventToken.generate()); EventToken.generate(),
event.setOrganizerToken(OrganizerToken.generate()); OrganizerToken.generate(),
event.setTitle("Test Event"); "Test Event",
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))); null,
event.setTimezone(ZONE); OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
event.setExpiryDate(TODAY.plusDays(30)); ZONE,
event.setCreatedAt(OffsetDateTime.now()); null,
return event; expiryDate,
OffsetDateTime.now(),
false,
null);
} }
} }