Refactor domain models to records and move exceptions to sub-package
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
@@ -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.library.Architectures.onionArchitecture;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchCondition;
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)
class HexagonalArchitectureTest {
@@ -65,4 +69,24 @@ class HexagonalArchitectureTest {
static final ArchRule webAdapterMustNotDependOnOutboundPorts = noClasses()
.that().resideInAPackage("de.fete.adapter.in.web..")
.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"));
}
}
};
}
}
@@ -42,7 +42,7 @@ class EventPersistenceAdapterIntegrationTest {
eventRepository.deleteExpired();
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
}
@Test
@@ -52,7 +52,7 @@ class EventPersistenceAdapterIntegrationTest {
eventRepository.deleteExpired();
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
}
@Test
@@ -66,16 +66,18 @@ class EventPersistenceAdapterIntegrationTest {
}
private Event buildEvent(String title, LocalDate expiryDate) {
var event = new Event();
event.setEventToken(EventToken.generate());
event.setOrganizerToken(OrganizerToken.generate());
event.setTitle(title);
event.setDescription("Test description");
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
event.setTimezone(ZoneId.of("Europe/Berlin"));
event.setLocation("Test Location");
event.setExpiryDate(expiryDate);
event.setCreatedAt(OffsetDateTime.now());
return event;
return new Event(
null,
EventToken.generate(),
OrganizerToken.generate(),
title,
"Test description",
OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
ZoneId.of("Europe/Berlin"),
"Test Location",
expiryDate,
OffsetDateTime.now(),
false,
null);
}
}
@@ -30,8 +30,8 @@ class EventPersistenceAdapterTest {
Event saved = eventRepository.save(event);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getTitle()).isEqualTo("Test Event");
assertThat(saved.id()).isNotNull();
assertThat(saved.title()).isEqualTo("Test Event");
}
@Test
@@ -39,11 +39,11 @@ class EventPersistenceAdapterTest {
Event event = buildEvent();
Event saved = eventRepository.save(event);
Optional<Event> found = eventRepository.findByEventToken(saved.getEventToken());
Optional<Event> found = eventRepository.findByEventToken(saved.eventToken());
assertThat(found).isPresent();
assertThat(found.get().getTitle()).isEqualTo("Test Event");
assertThat(found.get().getId()).isEqualTo(saved.getId());
assertThat(found.get().title()).isEqualTo("Test Event");
assertThat(found.get().id()).isEqualTo(saved.id());
}
@Test
@@ -61,42 +61,47 @@ class EventPersistenceAdapterTest {
OffsetDateTime createdAt =
OffsetDateTime.of(2026, 3, 4, 12, 0, 0, 0, ZoneOffset.UTC);
var event = new Event();
event.setEventToken(EventToken.generate());
event.setOrganizerToken(OrganizerToken.generate());
event.setTitle("Full Event");
event.setDescription("A detailed description");
event.setDateTime(dateTime);
event.setTimezone(ZoneId.of("Europe/Berlin"));
event.setLocation("Berlin, Germany");
event.setExpiryDate(expiryDate);
event.setCreatedAt(createdAt);
var event = new Event(
null,
EventToken.generate(),
OrganizerToken.generate(),
"Full Event",
"A detailed description",
dateTime,
ZoneId.of("Europe/Berlin"),
"Berlin, Germany",
expiryDate,
createdAt,
false,
null);
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.getOrganizerToken()).isEqualTo(event.getOrganizerToken());
assertThat(found.getTitle()).isEqualTo("Full Event");
assertThat(found.getDescription()).isEqualTo("A detailed description");
assertThat(found.getDateTime().toInstant()).isEqualTo(dateTime.toInstant());
assertThat(found.getTimezone()).isEqualTo(ZoneId.of("Europe/Berlin"));
assertThat(found.getLocation()).isEqualTo("Berlin, Germany");
assertThat(found.getExpiryDate()).isEqualTo(expiryDate);
assertThat(found.getCreatedAt().toInstant()).isEqualTo(createdAt.toInstant());
assertThat(found.eventToken()).isEqualTo(event.eventToken());
assertThat(found.organizerToken()).isEqualTo(event.organizerToken());
assertThat(found.title()).isEqualTo("Full Event");
assertThat(found.description()).isEqualTo("A detailed description");
assertThat(found.dateTime().toInstant()).isEqualTo(dateTime.toInstant());
assertThat(found.timezone()).isEqualTo(ZoneId.of("Europe/Berlin"));
assertThat(found.location()).isEqualTo("Berlin, Germany");
assertThat(found.expiryDate()).isEqualTo(expiryDate);
assertThat(found.createdAt().toInstant()).isEqualTo(createdAt.toInstant());
}
private Event buildEvent() {
var event = new Event();
event.setEventToken(EventToken.generate());
event.setOrganizerToken(OrganizerToken.generate());
event.setTitle("Test Event");
event.setDescription("Test description");
event.setDateTime(OffsetDateTime.now().plusDays(7));
event.setTimezone(ZoneId.of("Europe/Berlin"));
event.setLocation("Somewhere");
event.setExpiryDate(LocalDate.now().plusDays(30));
event.setCreatedAt(OffsetDateTime.now());
return event;
return new Event(
null,
EventToken.generate(),
OrganizerToken.generate(),
"Test Event",
"Test description",
OffsetDateTime.now().plusDays(7),
ZoneId.of("Europe/Berlin"),
"Somewhere",
LocalDate.now().plusDays(30),
OffsetDateTime.now(),
false,
null);
}
}
@@ -7,7 +7,9 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
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.EventToken;
import de.fete.domain.model.OrganizerToken;
@@ -46,10 +48,8 @@ class EventServiceCancelTest {
void cancelEventDelegatesToDomainAndSaves() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
null, false, null);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
@@ -60,18 +60,16 @@ class EventServiceCancelTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isEqualTo("Venue unavailable");
assertThat(captor.getValue().cancelled()).isTrue();
assertThat(captor.getValue().cancellationReason()).isEqualTo("Venue unavailable");
}
@Test
void cancelEventWithNullReason() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
null, false, null);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
@@ -82,8 +80,8 @@ class EventServiceCancelTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isNull();
assertThat(captor.getValue().cancelled()).isTrue();
assertThat(captor.getValue().cancellationReason()).isNull();
}
@Test
@@ -104,9 +102,8 @@ class EventServiceCancelTest {
void cancelEventThrows403WhenWrongOrganizerToken() {
EventToken eventToken = EventToken.generate();
OrganizerToken correctToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(correctToken);
var event = new Event(null, eventToken, correctToken, null, null, null, null, null, null,
null, false, null);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
@@ -122,10 +119,8 @@ class EventServiceCancelTest {
void cancelEventThrows409WhenAlreadyCancelled() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(true);
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
null, true, null);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
@@ -57,13 +57,13 @@ class EventServiceTest {
Event result = eventService.createEvent(command);
assertThat(result.getTitle()).isEqualTo("Birthday Party");
assertThat(result.getDescription()).isEqualTo("Come celebrate!");
assertThat(result.getTimezone()).isEqualTo(ZONE);
assertThat(result.getLocation()).isEqualTo("Berlin");
assertThat(result.getEventToken()).isNotNull();
assertThat(result.getOrganizerToken()).isNotNull();
assertThat(result.getCreatedAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE));
assertThat(result.title()).isEqualTo("Birthday Party");
assertThat(result.description()).isEqualTo("Come celebrate!");
assertThat(result.timezone()).isEqualTo(ZONE);
assertThat(result.location()).isEqualTo("Berlin");
assertThat(result.eventToken()).isNotNull();
assertThat(result.organizerToken()).isNotNull();
assertThat(result.createdAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE));
}
@Test
@@ -80,7 +80,7 @@ class EventServiceTest {
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository, times(1)).save(captor.capture());
assertThat(captor.getValue().getTitle()).isEqualTo("Test");
assertThat(captor.getValue().title()).isEqualTo("Test");
}
@Test
@@ -96,7 +96,7 @@ class EventServiceTest {
Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(eventDate.plusDays(7));
assertThat(result.expiryDate()).isEqualTo(eventDate.plusDays(7));
}
// --- GetEventUseCase tests (T004) ---
@@ -104,16 +104,15 @@ class EventServiceTest {
@Test
void getByEventTokenReturnsEvent() {
EventToken token = EventToken.generate();
var event = new Event();
event.setEventToken(token);
event.setTitle("Found Event");
var event = new Event(null, token, null, "Found Event", null, null, null, null, null, null,
false, null);
when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event));
Optional<Event> result = eventService.getByEventToken(token);
assertThat(result).isPresent();
assertThat(result.get().getTitle()).isEqualTo("Found Event");
assertThat(result.get().title()).isEqualTo("Found Event");
}
@Test
@@ -142,6 +141,6 @@ class EventServiceTest {
Event result = eventService.createEvent(command);
assertThat(result.getTimezone()).isEqualTo(ZoneId.of("America/New_York"));
assertThat(result.timezone()).isEqualTo(ZoneId.of("America/New_York"));
}
}
@@ -6,6 +6,10 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
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.EventToken;
import de.fete.domain.model.OrganizerToken;
@@ -51,23 +55,23 @@ class RsvpServiceTest {
@Test
void createRsvpSucceedsForActiveEvent() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
Rsvp result = rsvpService.createRsvp(token, "Max Mustermann");
assertThat(result.getName()).isEqualTo("Max Mustermann");
assertThat(result.getRsvpToken()).isNotNull();
assertThat(result.getEventId()).isEqualTo(event.getId());
assertThat(result.name()).isEqualTo("Max Mustermann");
assertThat(result.rsvpToken()).isNotNull();
assertThat(result.eventId()).isEqualTo(event.id());
}
@Test
void createRsvpPersistsViaRepository() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
@@ -76,8 +80,8 @@ class RsvpServiceTest {
ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class);
verify(rsvpRepository).save(captor.capture());
assertThat(captor.getValue().getName()).isEqualTo("Test Guest");
assertThat(captor.getValue().getEventId()).isEqualTo(event.getId());
assertThat(captor.getValue().name()).isEqualTo("Test Guest");
assertThat(captor.getValue().eventId()).isEqualTo(event.id());
}
@Test
@@ -91,22 +95,21 @@ class RsvpServiceTest {
@Test
void createRsvpTrimsName() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
when(rsvpRepository.save(any(Rsvp.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
Rsvp result = rsvpService.createRsvp(token, " Max ");
assertThat(result.getName()).isEqualTo("Max");
assertThat(result.name()).isEqualTo("Max");
}
@Test
void createRsvpThrowsWhenEventExpired() {
var event = buildActiveEvent();
event.setExpiryDate(TODAY.minusDays(1));
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.minusDays(1));
EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
@@ -115,9 +118,8 @@ class RsvpServiceTest {
@Test
void createRsvpThrowsWhenEventExpiresToday() {
var event = buildActiveEvent();
event.setExpiryDate(TODAY);
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY);
EventToken token = event.eventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
@@ -126,12 +128,12 @@ class RsvpServiceTest {
@Test
void getAttendeeNamesReturnsNamesInOrder() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
OrganizerToken orgToken = event.getOrganizerToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
OrganizerToken orgToken = event.organizerToken();
when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event));
when(rsvpRepository.findByEventId(event.getId()))
when(rsvpRepository.findByEventId(event.id()))
.thenReturn(List.of(
buildRsvp(1L, "Alice"),
buildRsvp(2L, "Bob"),
@@ -144,12 +146,12 @@ class RsvpServiceTest {
@Test
void getAttendeeNamesReturnsEmptyListWhenNoRsvps() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
OrganizerToken orgToken = event.getOrganizerToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
OrganizerToken orgToken = event.organizerToken();
when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event));
when(rsvpRepository.findByEventId(event.getId()))
when(rsvpRepository.findByEventId(event.id()))
.thenReturn(List.of());
List<String> names = rsvpService.getAttendeeNames(token, orgToken);
@@ -171,8 +173,8 @@ class RsvpServiceTest {
@Test
void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
OrganizerToken wrongToken = OrganizerToken.generate();
when(eventRepository.findByEventToken(token))
.thenReturn(Optional.of(event));
@@ -183,38 +185,33 @@ class RsvpServiceTest {
}
private Rsvp buildRsvp(Long id, String name) {
var rsvp = new Rsvp();
rsvp.setId(id);
rsvp.setRsvpToken(RsvpToken.generate());
rsvp.setEventId(1L);
rsvp.setName(name);
return rsvp;
return new Rsvp(id, RsvpToken.generate(), 1L, name);
}
@Test
void cancelRsvpDeletesWhenEventAndRsvpExist() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
RsvpToken rsvpToken = RsvpToken.generate();
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);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
}
@Test
void cancelRsvpSucceedsWhenRsvpNotFound() {
Event event = buildActiveEvent();
EventToken token = event.getEventToken();
Event event = buildActiveEvent(TODAY.plusDays(30));
EventToken token = event.eventToken();
RsvpToken rsvpToken = RsvpToken.generate();
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);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken);
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
}
@Test
@@ -226,16 +223,19 @@ class RsvpServiceTest {
rsvpService.cancelRsvp(token, rsvpToken);
}
private Event buildActiveEvent() {
var event = new Event();
event.setId(1L);
event.setEventToken(EventToken.generate());
event.setOrganizerToken(OrganizerToken.generate());
event.setTitle("Test Event");
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
event.setTimezone(ZONE);
event.setExpiryDate(TODAY.plusDays(30));
event.setCreatedAt(OffsetDateTime.now());
return event;
private Event buildActiveEvent(LocalDate expiryDate) {
return new Event(
1L,
EventToken.generate(),
OrganizerToken.generate(),
"Test Event",
null,
OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
ZONE,
null,
expiryDate,
OffsetDateTime.now(),
false,
null);
}
}