Block RSVPs on expired events with 409 Conflict and inject Clock into RsvpService

Adds expiry check to RsvpService using an injected Clock for testability,
handles EventExpiredException in GlobalExceptionHandler as 409 Conflict,
and adds unit + integration tests using relative dates from a fixed clock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 13:00:30 +01:00
parent be1c5062a2
commit 4d6df8d16b
4 changed files with 69 additions and 3 deletions

View File

@@ -349,6 +349,22 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found"));
}
@Test
void createRsvpForExpiredEventReturns409() throws Exception {
EventJpaEntity event = seedEvent(
"Expired Party", null, "Europe/Berlin",
null, LocalDate.now().minusDays(1));
var request = new CreateRsvpRequest().name("Late Guest");
mockMvc.perform(post("/api/events/" + event.getEventToken() + "/rsvps")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:event-expired"));
}
private EventJpaEntity seedEvent(
String title, String description, String timezone,
String location, LocalDate expiryDate) {

View File

@@ -12,6 +12,8 @@ import de.fete.domain.model.OrganizerToken;
import de.fete.domain.model.Rsvp;
import de.fete.domain.port.out.EventRepository;
import de.fete.domain.port.out.RsvpRepository;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
@@ -28,6 +30,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
class RsvpServiceTest {
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
private static final Instant NOW = Instant.parse("2026-03-08T12:00:00Z");
private static final Clock FIXED_CLOCK = Clock.fixed(NOW, ZONE);
private static final LocalDate TODAY = LocalDate.ofInstant(NOW, ZONE);
@Mock
private EventRepository eventRepository;
@@ -39,7 +44,7 @@ class RsvpServiceTest {
@BeforeEach
void setUp() {
rsvpService = new RsvpService(eventRepository, rsvpRepository);
rsvpService = new RsvpService(eventRepository, rsvpRepository, FIXED_CLOCK);
}
@Test
@@ -95,6 +100,28 @@ class RsvpServiceTest {
assertThat(result.getName()).isEqualTo("Max");
}
@Test
void createRsvpThrowsWhenEventExpired() {
var event = buildActiveEvent();
event.setExpiryDate(TODAY.minusDays(1));
EventToken token = event.getEventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
.isInstanceOf(EventExpiredException.class);
}
@Test
void createRsvpThrowsWhenEventExpiresToday() {
var event = buildActiveEvent();
event.setExpiryDate(TODAY);
EventToken token = event.getEventToken();
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
.isInstanceOf(EventExpiredException.class);
}
private Event buildActiveEvent() {
var event = new Event();
event.setId(1L);
@@ -103,7 +130,7 @@ class RsvpServiceTest {
event.setTitle("Test Event");
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
event.setTimezone(ZONE);
event.setExpiryDate(LocalDate.of(2026, 7, 15));
event.setExpiryDate(TODAY.plusDays(30));
event.setCreatedAt(OffsetDateTime.now());
return event;
}