Merge pull request 'Make expiryDate an internal concern, auto-set to event date + 7 days' (#25) from auto-expiry-date into master
All checks were successful
CI / backend-test (push) Successful in 59s
CI / frontend-test (push) Successful in 23s
CI / frontend-e2e (push) Successful in 1m9s
CI / build-and-publish (push) Has been skipped

This commit was merged in pull request #25.
This commit is contained in:
2026-03-09 21:33:43 +01:00
18 changed files with 33 additions and 400 deletions

View File

@@ -20,9 +20,7 @@ import de.fete.domain.port.in.CreateEventUseCase;
import de.fete.domain.port.in.CreateRsvpUseCase; import de.fete.domain.port.in.CreateRsvpUseCase;
import de.fete.domain.port.in.GetAttendeesUseCase; import de.fete.domain.port.in.GetAttendeesUseCase;
import de.fete.domain.port.in.GetEventUseCase; import de.fete.domain.port.in.GetEventUseCase;
import java.time.Clock;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -39,22 +37,19 @@ public class EventController implements EventsApi {
private final CreateRsvpUseCase createRsvpUseCase; private final CreateRsvpUseCase createRsvpUseCase;
private final CountAttendeesByEventUseCase countAttendeesByEventUseCase; private final CountAttendeesByEventUseCase countAttendeesByEventUseCase;
private final GetAttendeesUseCase getAttendeesUseCase; private final GetAttendeesUseCase getAttendeesUseCase;
private final Clock clock;
/** Creates a new controller with the given use cases and clock. */ /** Creates a new controller with the given use cases. */
public EventController( public EventController(
CreateEventUseCase createEventUseCase, CreateEventUseCase createEventUseCase,
GetEventUseCase getEventUseCase, GetEventUseCase getEventUseCase,
CreateRsvpUseCase createRsvpUseCase, CreateRsvpUseCase createRsvpUseCase,
CountAttendeesByEventUseCase countAttendeesByEventUseCase, CountAttendeesByEventUseCase countAttendeesByEventUseCase,
GetAttendeesUseCase getAttendeesUseCase, GetAttendeesUseCase getAttendeesUseCase) {
Clock clock) {
this.createEventUseCase = createEventUseCase; this.createEventUseCase = createEventUseCase;
this.getEventUseCase = getEventUseCase; this.getEventUseCase = getEventUseCase;
this.createRsvpUseCase = createRsvpUseCase; this.createRsvpUseCase = createRsvpUseCase;
this.countAttendeesByEventUseCase = countAttendeesByEventUseCase; this.countAttendeesByEventUseCase = countAttendeesByEventUseCase;
this.getAttendeesUseCase = getAttendeesUseCase; this.getAttendeesUseCase = getAttendeesUseCase;
this.clock = clock;
} }
@Override @Override
@@ -67,8 +62,7 @@ public class EventController implements EventsApi {
request.getDescription(), request.getDescription(),
request.getDateTime(), request.getDateTime(),
zoneId, zoneId,
request.getLocation(), request.getLocation()
request.getExpiryDate()
); );
Event event = createEventUseCase.createEvent(command); Event event = createEventUseCase.createEvent(command);
@@ -79,7 +73,6 @@ public class EventController implements EventsApi {
response.setTitle(event.getTitle()); response.setTitle(event.getTitle());
response.setDateTime(event.getDateTime()); response.setDateTime(event.getDateTime());
response.setTimezone(event.getTimezone().getId()); response.setTimezone(event.getTimezone().getId());
response.setExpiryDate(event.getExpiryDate());
return ResponseEntity.status(HttpStatus.CREATED).body(response); return ResponseEntity.status(HttpStatus.CREATED).body(response);
} }
@@ -99,8 +92,6 @@ public class EventController implements EventsApi {
response.setLocation(event.getLocation()); response.setLocation(event.getLocation());
response.setAttendeeCount( response.setAttendeeCount(
(int) countAttendeesByEventUseCase.countByEvent(eventToken)); (int) countAttendeesByEventUseCase.countByEvent(eventToken));
response.setExpired(
event.getExpiryDate().isBefore(LocalDate.now(clock)));
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }

View File

@@ -17,6 +17,8 @@ import org.springframework.stereotype.Service;
@Service @Service
public class EventService implements CreateEventUseCase, GetEventUseCase { public class EventService implements CreateEventUseCase, GetEventUseCase {
private static final int EXPIRY_DAYS_AFTER_EVENT = 7;
private final EventRepository eventRepository; private final EventRepository eventRepository;
private final Clock clock; private final Clock clock;
@@ -28,13 +30,7 @@ public class EventService implements CreateEventUseCase, GetEventUseCase {
@Override @Override
public Event createEvent(CreateEventCommand command) { public Event createEvent(CreateEventCommand command) {
if (!command.expiryDate().isAfter(LocalDate.now(clock))) { LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
throw new ExpiryDateInPastException(command.expiryDate());
}
if (!command.expiryDate().isAfter(command.dateTime().toLocalDate())) {
throw new ExpiryDateBeforeEventException(command.expiryDate(), command.dateTime());
}
var event = new Event(); var event = new Event();
event.setEventToken(EventToken.generate()); event.setEventToken(EventToken.generate());
@@ -44,7 +40,7 @@ public class EventService implements CreateEventUseCase, GetEventUseCase {
event.setDateTime(command.dateTime()); event.setDateTime(command.dateTime());
event.setTimezone(command.timezone()); event.setTimezone(command.timezone());
event.setLocation(command.location()); event.setLocation(command.location());
event.setExpiryDate(command.expiryDate()); event.setExpiryDate(expiryDate);
event.setCreatedAt(OffsetDateTime.now(clock)); event.setCreatedAt(OffsetDateTime.now(clock));
return eventRepository.save(event); return eventRepository.save(event);

View File

@@ -10,6 +10,5 @@ public record CreateEventCommand(
String description, String description,
OffsetDateTime dateTime, OffsetDateTime dateTime,
ZoneId timezone, ZoneId timezone,
String location, String location
LocalDate expiryDate
) {} ) {}

View File

@@ -160,7 +160,6 @@ components:
- title - title
- dateTime - dateTime
- timezone - timezone
- expiryDate
properties: properties:
title: title:
type: string type: string
@@ -181,11 +180,6 @@ components:
location: location:
type: string type: string
maxLength: 500 maxLength: 500
expiryDate:
type: string
format: date
description: Date after which event data is deleted. Must be in the future.
example: "2026-06-15"
CreateEventResponse: CreateEventResponse:
type: object type: object
@@ -195,7 +189,6 @@ components:
- title - title
- dateTime - dateTime
- timezone - timezone
- expiryDate
properties: properties:
eventToken: eventToken:
type: string type: string
@@ -218,10 +211,6 @@ components:
type: string type: string
description: IANA timezone of the organizer description: IANA timezone of the organizer
example: "Europe/Berlin" example: "Europe/Berlin"
expiryDate:
type: string
format: date
example: "2026-06-15"
GetEventResponse: GetEventResponse:
type: object type: object
@@ -231,7 +220,6 @@ components:
- dateTime - dateTime
- timezone - timezone
- attendeeCount - attendeeCount
- expired
properties: properties:
eventToken: eventToken:
type: string type: string
@@ -264,10 +252,6 @@ components:
minimum: 0 minimum: 0
description: Number of confirmed attendees (attending=true) description: Number of confirmed attendees (attending=true)
example: 12 example: 12
expired:
type: boolean
description: Whether the event's expiry date has passed
example: false
CreateRsvpRequest: CreateRsvpRequest:
type: object type: object

View File

@@ -55,8 +55,7 @@ class EventControllerIntegrationTest {
.description("Come celebrate!") .description("Come celebrate!")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))) .dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin") .timezone("Europe/Berlin")
.location("Berlin") .location("Berlin");
.expiryDate(LocalDate.of(2026, 6, 16));
var result = mockMvc.perform(post("/api/events") var result = mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -67,7 +66,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.title").value("Birthday Party")) .andExpect(jsonPath("$.title").value("Birthday Party"))
.andExpect(jsonPath("$.timezone").value("Europe/Berlin")) .andExpect(jsonPath("$.timezone").value("Europe/Berlin"))
.andExpect(jsonPath("$.dateTime").isNotEmpty()) .andExpect(jsonPath("$.dateTime").isNotEmpty())
.andExpect(jsonPath("$.expiryDate").isNotEmpty())
.andReturn(); .andReturn();
var response = objectMapper.readValue( var response = objectMapper.readValue(
@@ -79,7 +77,7 @@ class EventControllerIntegrationTest {
assertThat(persisted.getDescription()).isEqualTo("Come celebrate!"); assertThat(persisted.getDescription()).isEqualTo("Come celebrate!");
assertThat(persisted.getTimezone()).isEqualTo("Europe/Berlin"); assertThat(persisted.getTimezone()).isEqualTo("Europe/Berlin");
assertThat(persisted.getLocation()).isEqualTo("Berlin"); assertThat(persisted.getLocation()).isEqualTo("Berlin");
assertThat(persisted.getExpiryDate()).isEqualTo(request.getExpiryDate()); assertThat(persisted.getExpiryDate()).isEqualTo(LocalDate.of(2026, 6, 22));
assertThat(persisted.getDateTime().toInstant()) assertThat(persisted.getDateTime().toInstant())
.isEqualTo(request.getDateTime().toInstant()); .isEqualTo(request.getDateTime().toInstant());
assertThat(persisted.getOrganizerToken()).isNotNull(); assertThat(persisted.getOrganizerToken()).isNotNull();
@@ -91,8 +89,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest() var request = new CreateEventRequest()
.title("Minimal Event") .title("Minimal Event")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))) .dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("UTC") .timezone("UTC");
.expiryDate(LocalDate.of(2026, 6, 16));
var result = mockMvc.perform(post("/api/events") var result = mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -119,8 +116,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest() var request = new CreateEventRequest()
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))) .dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin") .timezone("Europe/Berlin");
.expiryDate(LocalDate.of(2026, 6, 16));
mockMvc.perform(post("/api/events") mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -139,26 +135,6 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest() var request = new CreateEventRequest()
.title("No Date") .title("No Date")
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 16));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.fieldErrors").isArray());
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventMissingExpiryDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("No Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin"); .timezone("Europe/Berlin");
mockMvc.perform(post("/api/events") mockMvc.perform(post("/api/events")
@@ -171,93 +147,12 @@ class EventControllerIntegrationTest {
assertThat(jpaRepository.count()).isEqualTo(countBefore); assertThat(jpaRepository.count()).isEqualTo(countBefore);
} }
@Test
void createEventExpiryDateInPastReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Past Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2025, 1, 1));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-in-past"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateTodayReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Today Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.now());
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-in-past"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateBeforeEventDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Bad Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 10));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-before-event"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateSameAsEventDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Same Day Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 15));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-before-event"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test @Test
void errorResponseContentTypeIsProblemJson() throws Exception { void errorResponseContentTypeIsProblemJson() throws Exception {
var request = new CreateEventRequest() var request = new CreateEventRequest()
.title("") .title("")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))) .dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin") .timezone("Europe/Berlin");
.expiryDate(LocalDate.of(2026, 6, 16));
mockMvc.perform(post("/api/events") mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -273,8 +168,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest() var request = new CreateEventRequest()
.title("Bad TZ") .title("Bad TZ")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))) .dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Not/A/Zone") .timezone("Not/A/Zone");
.expiryDate(LocalDate.of(2026, 6, 16));
mockMvc.perform(post("/api/events") mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
@@ -302,7 +196,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.timezone").value("Europe/Berlin")) .andExpect(jsonPath("$.timezone").value("Europe/Berlin"))
.andExpect(jsonPath("$.location").value("Central Park")) .andExpect(jsonPath("$.location").value("Central Park"))
.andExpect(jsonPath("$.attendeeCount").value(0)) .andExpect(jsonPath("$.attendeeCount").value(0))
.andExpect(jsonPath("$.expired").value(false))
.andExpect(jsonPath("$.dateTime").isNotEmpty()); .andExpect(jsonPath("$.dateTime").isNotEmpty());
} }
@@ -327,18 +220,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found")); .andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found"));
} }
@Test
void getExpiredEventReturnsExpiredTrue() throws Exception {
EventJpaEntity entity = seedEvent(
"Past Event", "It happened", "Europe/Berlin",
"Old Venue", LocalDate.now().minusDays(1));
mockMvc.perform(get("/api/events/" + entity.getEventToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Past Event"))
.andExpect(jsonPath("$.expired").value(true));
}
// --- RSVP tests --- // --- RSVP tests ---
@Test @Test

View File

@@ -1,7 +1,6 @@
package de.fete.application.service; package de.fete.application.service;
import static org.assertj.core.api.Assertions.assertThat; 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.any;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -53,8 +52,7 @@ class EventServiceTest {
"Come celebrate!", "Come celebrate!",
TODAY.plusDays(90).atStartOfDay(ZONE).toOffsetDateTime(), TODAY.plusDays(90).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, ZONE,
"Berlin", "Berlin"
TODAY.plusDays(120)
); );
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);
@@ -75,8 +73,7 @@ class EventServiceTest {
var command = new CreateEventCommand( var command = new CreateEventCommand(
"Test", null, "Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null, TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null
TODAY.plusDays(11)
); );
eventService.createEvent(command); eventService.createEvent(command);
@@ -87,86 +84,19 @@ class EventServiceTest {
} }
@Test @Test
void expiryDateTodayThrowsException() { void expiryDateIsEventDatePlusSevenDays() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateInPastException.class);
}
@Test
void expiryDateInPastThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY.minusDays(5)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateInPastException.class);
}
@Test
void expiryDateTomorrowSucceeds() {
when(eventRepository.save(any(Event.class))) when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0)); .thenAnswer(invocation -> invocation.getArgument(0));
var eventDate = TODAY.plusDays(10);
var command = new CreateEventCommand( var command = new CreateEventCommand(
"Test", null, "Test", null,
TODAY.plusDays(1).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null, eventDate.atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null
TODAY.plusDays(2)
); );
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(TODAY.plusDays(2)); assertThat(result.getExpiryDate()).isEqualTo(eventDate.plusDays(7));
}
@Test
void expiryDateSameAsEventDateThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(10)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateBeforeEventException.class);
}
@Test
void expiryDateBeforeEventDateThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(5)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateBeforeEventException.class);
}
@Test
void expiryDateDayAfterEventDateSucceeds() {
when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(11)
);
Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(TODAY.plusDays(11));
} }
// --- GetEventUseCase tests (T004) --- // --- GetEventUseCase tests (T004) ---
@@ -207,8 +137,7 @@ class EventServiceTest {
var command = new CreateEventCommand( var command = new CreateEventCommand(
"Test", null, "Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZoneId.of("America/New_York"), null, ZoneId.of("America/New_York"), null
TODAY.plusDays(11)
); );
Event result = eventService.createEvent(command); Event result = eventService.createEvent(command);

View File

@@ -9,7 +9,6 @@ test.describe('US-1: Create an event', () => {
await expect(page.getByText('Title is required.')).toBeVisible() await expect(page.getByText('Title is required.')).toBeVisible()
await expect(page.getByText('Date and time are required.')).toBeVisible() await expect(page.getByText('Date and time are required.')).toBeVisible()
await expect(page.getByText('Expiry date is required.')).toBeVisible()
}) })
test('creates an event and redirects to event detail page', async ({ page }) => { test('creates an event and redirects to event detail page', async ({ page }) => {
@@ -19,7 +18,6 @@ test.describe('US-1: Create an event', () => {
await page.getByLabel(/description/i).fill('Bring your own drinks') await page.getByLabel(/description/i).fill('Bring your own drinks')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00') await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/location/i).fill('Central Park') await page.getByLabel(/location/i).fill('Central Park')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click() await page.getByRole('button', { name: /create event/i }).click()
@@ -31,7 +29,6 @@ test.describe('US-1: Create an event', () => {
await page.getByLabel(/title/i).fill('Summer BBQ') await page.getByLabel(/title/i).fill('Summer BBQ')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00') await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click() await page.getByRole('button', { name: /create event/i }).click()
await expect(page).toHaveURL(/\/events\/.+/) await expect(page).toHaveURL(/\/events\/.+/)
@@ -59,7 +56,6 @@ test.describe('US-1: Create an event', () => {
await page.goto('/create') await page.goto('/create')
await page.getByLabel(/title/i).fill('Test') await page.getByLabel(/title/i).fill('Test')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00') await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click() await page.getByRole('button', { name: /create event/i }).click()

View File

@@ -9,7 +9,6 @@ const fullEvent = {
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
location: 'Central Park, NYC', location: 'Central Park, NYC',
attendeeCount: 12, attendeeCount: 12,
expired: false,
} }
test.describe('US1: RSVP submission flow', () => { test.describe('US1: RSVP submission flow', () => {
@@ -170,16 +169,4 @@ test.describe('US1: RSVP submission flow', () => {
await expect(page.getByText("You're attending!")).not.toBeVisible() await expect(page.getByText("You're attending!")).not.toBeVisible()
}) })
test('does not show RSVP bar on expired event', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => {
return HttpResponse.json({ ...fullEvent, expired: true })
}),
)
await page.goto(`/events/${fullEvent.eventToken}`)
await expect(page.getByText('This event has ended.')).toBeVisible()
await expect(page.getByRole('button', { name: "I'm attending" })).not.toBeVisible()
})
}) })

View File

@@ -9,7 +9,6 @@ const fullEvent = {
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
location: 'Central Park, NYC', location: 'Central Park, NYC',
attendeeCount: 12, attendeeCount: 12,
expired: false,
} }
test.describe('US-1: View event details', () => { test.describe('US-1: View event details', () => {
@@ -52,20 +51,6 @@ test.describe('US-1: View event details', () => {
}) })
}) })
test.describe('US-2: View expired event', () => {
test('shows "event has ended" banner for expired event', async ({ page, network }) => {
network.use(
http.get('*/api/events/:token', () => {
return HttpResponse.json({ ...fullEvent, expired: true })
}),
)
await page.goto(`/events/${fullEvent.eventToken}`)
await expect(page.getByText('This event has ended.')).toBeVisible()
})
})
test.describe('US-4: Event not found', () => { test.describe('US-4: Event not found', () => {
test('shows "event not found" for unknown token', async ({ page, network }) => { test('shows "event not found" for unknown token', async ({ page, network }) => {
network.use( network.use(

View File

@@ -7,7 +7,6 @@ const futureEvent1: StoredEvent = {
eventToken: 'future-aaa', eventToken: 'future-aaa',
title: 'Summer BBQ', title: 'Summer BBQ',
dateTime: '2027-06-15T18:00:00Z', dateTime: '2027-06-15T18:00:00Z',
expiryDate: '2027-06-16T00:00:00Z',
organizerToken: 'org-token-1', organizerToken: 'org-token-1',
} }
@@ -15,7 +14,6 @@ const futureEvent2: StoredEvent = {
eventToken: 'future-bbb', eventToken: 'future-bbb',
title: 'Team Meeting', title: 'Team Meeting',
dateTime: '2027-01-10T09:00:00Z', dateTime: '2027-01-10T09:00:00Z',
expiryDate: '2027-01-11T00:00:00Z',
rsvpToken: 'rsvp-token-1', rsvpToken: 'rsvp-token-1',
rsvpName: 'Alice', rsvpName: 'Alice',
} }
@@ -24,7 +22,6 @@ const pastEvent: StoredEvent = {
eventToken: 'past-ccc', eventToken: 'past-ccc',
title: 'New Year Party', title: 'New Year Party',
dateTime: '2025-01-01T00:00:00Z', dateTime: '2025-01-01T00:00:00Z',
expiryDate: '2025-01-02T00:00:00Z',
} }
function seedEvents(events: StoredEvent[]): string { function seedEvents(events: StoredEvent[]): string {
@@ -85,7 +82,6 @@ test.describe('US4: Past Events Appear Faded', () => {
location: '', location: '',
timezone: 'UTC', timezone: 'UTC',
attendeeCount: 0, attendeeCount: 0,
expired: true,
}) })
}), }),
) )
@@ -199,13 +195,11 @@ test.describe('Temporal Grouping: Section Headers', () => {
eventToken: 'today-1', eventToken: 'today-1',
title: 'Today Standup', title: 'Today Standup',
dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0, 0).toISOString(), dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0, 0).toISOString(),
expiryDate: '',
} }
const laterEvent: StoredEvent = { const laterEvent: StoredEvent = {
eventToken: 'later-1', eventToken: 'later-1',
title: 'Future Conference', title: 'Future Conference',
dateTime: new Date(now.getFullYear() + 1, 0, 15, 10, 0, 0).toISOString(), dateTime: new Date(now.getFullYear() + 1, 0, 15, 10, 0, 0).toISOString(),
expiryDate: '',
} }
await page.addInitScript(seedEvents([todayEvent, laterEvent, pastEvent])) await page.addInitScript(seedEvents([todayEvent, laterEvent, pastEvent]))
await page.goto('/') await page.goto('/')
@@ -245,7 +239,6 @@ test.describe('Temporal Grouping: Section Headers', () => {
eventToken: 'today-emph', eventToken: 'today-emph',
title: 'Emphasis Test', title: 'Emphasis Test',
dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 0, 0).toISOString(), dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 0, 0).toISOString(),
expiryDate: '',
} }
await page.addInitScript(seedEvents([todayEvent])) await page.addInitScript(seedEvents([todayEvent]))
await page.goto('/') await page.goto('/')
@@ -262,7 +255,6 @@ test.describe('Temporal Grouping: Date Subheaders', () => {
eventToken: 'today-sub', eventToken: 'today-sub',
title: 'No Subheader Test', title: 'No Subheader Test',
dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 19, 0, 0).toISOString(), dateTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 19, 0, 0).toISOString(),
expiryDate: '',
} }
await page.addInitScript(seedEvents([todayEvent])) await page.addInitScript(seedEvents([todayEvent]))
await page.goto('/') await page.goto('/')
@@ -355,7 +347,6 @@ test.describe('US1: View My Events', () => {
location: '', location: '',
timezone: 'UTC', timezone: 'UTC',
attendeeCount: 0, attendeeCount: 0,
expired: false,
}) })
}), }),
) )

View File

@@ -15,11 +15,11 @@ const router = createRouter({
const NOW = new Date(2026, 2, 11, 12, 0, 0) const NOW = new Date(2026, 2, 11, 12, 0, 0)
const mockEvents = [ const mockEvents = [
{ eventToken: 'past-1', title: 'Past Event', dateTime: '2026-03-01T10:00:00', expiryDate: '' }, { eventToken: 'past-1', title: 'Past Event', dateTime: '2026-03-01T10:00:00' },
{ eventToken: 'later-1', title: 'Later Event', dateTime: '2027-06-15T10:00:00', expiryDate: '' }, { eventToken: 'later-1', title: 'Later Event', dateTime: '2027-06-15T10:00:00' },
{ eventToken: 'today-1', title: 'Today Event', dateTime: '2026-03-11T18:00:00', expiryDate: '' }, { eventToken: 'today-1', title: 'Today Event', dateTime: '2026-03-11T18:00:00' },
{ eventToken: 'week-1', title: 'This Week Event', dateTime: '2026-03-13T10:00:00', expiryDate: '' }, { eventToken: 'week-1', title: 'This Week Event', dateTime: '2026-03-13T10:00:00' },
{ eventToken: 'nextweek-1', title: 'Next Week Event', dateTime: '2026-03-16T10:00:00', expiryDate: '' }, { eventToken: 'nextweek-1', title: 'Next Week Event', dateTime: '2026-03-16T10:00:00' },
] ]
vi.mock('../../composables/useEventStorage', () => ({ vi.mock('../../composables/useEventStorage', () => ({

View File

@@ -6,7 +6,6 @@ function makeEvent(overrides: Partial<StoredEvent> & { dateTime: string }): Stor
return { return {
eventToken: `evt-${Math.random().toString(36).slice(2, 8)}`, eventToken: `evt-${Math.random().toString(36).slice(2, 8)}`,
title: 'Test Event', title: 'Test Event',
expiryDate: '',
...overrides, ...overrides,
} }
} }

View File

@@ -43,7 +43,6 @@ describe('useEventStorage', () => {
organizerToken: 'org-456', organizerToken: 'org-456',
title: 'Birthday', title: 'Birthday',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
const events = getStoredEvents() const events = getStoredEvents()
@@ -61,7 +60,6 @@ describe('useEventStorage', () => {
organizerToken: 'org-456', organizerToken: 'org-456',
title: 'Test', title: 'Test',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
expect(getOrganizerToken('abc-123')).toBe('org-456') expect(getOrganizerToken('abc-123')).toBe('org-456')
@@ -79,14 +77,12 @@ describe('useEventStorage', () => {
eventToken: 'event-1', eventToken: 'event-1',
title: 'First', title: 'First',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
saveCreatedEvent({ saveCreatedEvent({
eventToken: 'event-2', eventToken: 'event-2',
title: 'Second', title: 'Second',
dateTime: '2026-07-15T20:00:00+02:00', dateTime: '2026-07-15T20:00:00+02:00',
expiryDate: '2026-08-15',
}) })
const events = getStoredEvents() const events = getStoredEvents()
@@ -102,14 +98,12 @@ describe('useEventStorage', () => {
eventToken: 'abc-123', eventToken: 'abc-123',
title: 'Old Title', title: 'Old Title',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
saveCreatedEvent({ saveCreatedEvent({
eventToken: 'abc-123', eventToken: 'abc-123',
title: 'New Title', title: 'New Title',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
const events = getStoredEvents() const events = getStoredEvents()
@@ -124,7 +118,6 @@ describe('useEventStorage', () => {
eventToken: 'abc-123', eventToken: 'abc-123',
title: 'Birthday', title: 'Birthday',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
saveRsvp('abc-123', 'rsvp-token-1', 'Max', 'Birthday', '2026-06-15T20:00:00+02:00') saveRsvp('abc-123', 'rsvp-token-1', 'Max', 'Birthday', '2026-06-15T20:00:00+02:00')
@@ -154,7 +147,6 @@ describe('useEventStorage', () => {
eventToken: 'abc-123', eventToken: 'abc-123',
title: 'Test', title: 'Test',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
expect(getRsvp('abc-123')).toBeUndefined() expect(getRsvp('abc-123')).toBeUndefined()
@@ -172,14 +164,12 @@ describe('useEventStorage', () => {
eventToken: 'event-1', eventToken: 'event-1',
title: 'First', title: 'First',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
saveCreatedEvent({ saveCreatedEvent({
eventToken: 'event-2', eventToken: 'event-2',
title: 'Second', title: 'Second',
dateTime: '2026-07-15T20:00:00+02:00', dateTime: '2026-07-15T20:00:00+02:00',
expiryDate: '2026-08-15',
}) })
removeEvent('event-1') removeEvent('event-1')
@@ -196,7 +186,6 @@ describe('useEventStorage', () => {
eventToken: 'event-1', eventToken: 'event-1',
title: 'First', title: 'First',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}) })
removeEvent('nonexistent') removeEvent('nonexistent')
@@ -220,7 +209,6 @@ describe('isValidStoredEvent', () => {
eventToken: 'abc-123', eventToken: 'abc-123',
title: 'Birthday', title: 'Birthday',
dateTime: '2026-06-15T20:00:00+02:00', dateTime: '2026-06-15T20:00:00+02:00',
expiryDate: '2026-07-15',
}), }),
).toBe(true) ).toBe(true)
}) })

View File

@@ -3,7 +3,6 @@ export interface StoredEvent {
organizerToken?: string organizerToken?: string
title: string title: string
dateTime: string dateTime: string
expiryDate: string
rsvpToken?: string rsvpToken?: string
rsvpName?: string rsvpName?: string
} }
@@ -66,7 +65,7 @@ export function useEventStorage() {
existing.rsvpToken = rsvpToken existing.rsvpToken = rsvpToken
existing.rsvpName = rsvpName existing.rsvpName = rsvpName
} else { } else {
events.push({ eventToken, title, dateTime, expiryDate: '', rsvpToken, rsvpName }) events.push({ eventToken, title, dateTime, rsvpToken, rsvpName })
} }
writeEvents(events) writeEvents(events)
} }

View File

@@ -65,21 +65,6 @@
<span v-if="errors.location" id="location-error" class="field-error" role="alert">{{ errors.location }}</span> <span v-if="errors.location" id="location-error" class="field-error" role="alert">{{ errors.location }}</span>
</div> </div>
<div class="form-group">
<label for="expiryDate" class="form-label">Expiry Date *</label>
<input
id="expiryDate"
v-model="form.expiryDate"
type="date"
class="form-field glass"
required
:min="tomorrow"
:aria-invalid="!!errors.expiryDate"
:aria-describedby="errors.expiryDate ? 'expiryDate-error' : undefined"
/>
<span v-if="errors.expiryDate" id="expiryDate-error" class="field-error" role="alert">{{ errors.expiryDate }}</span>
</div>
<button type="submit" class="btn-primary glass" :disabled="submitting"> <button type="submit" class="btn-primary glass" :disabled="submitting">
{{ submitting ? 'Creating…' : 'Create Event' }} {{ submitting ? 'Creating…' : 'Create Event' }}
</button> </button>
@@ -90,7 +75,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, computed, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { RouterLink, useRouter } from 'vue-router' import { RouterLink, useRouter } from 'vue-router'
import { api } from '@/api/client' import { api } from '@/api/client'
import { useEventStorage } from '@/composables/useEventStorage' import { useEventStorage } from '@/composables/useEventStorage'
@@ -103,7 +88,6 @@ const form = reactive({
description: '', description: '',
dateTime: '', dateTime: '',
location: '', location: '',
expiryDate: '',
}) })
const errors = reactive({ const errors = reactive({
@@ -111,31 +95,22 @@ const errors = reactive({
description: '', description: '',
dateTime: '', dateTime: '',
location: '', location: '',
expiryDate: '',
}) })
const submitting = ref(false) const submitting = ref(false)
const serverError = ref('') const serverError = ref('')
const tomorrow = computed(() => {
const d = new Date()
d.setDate(d.getDate() + 1)
return d.toISOString().split('T')[0]
})
function clearErrors() { function clearErrors() {
errors.title = '' errors.title = ''
errors.description = '' errors.description = ''
errors.dateTime = '' errors.dateTime = ''
errors.location = '' errors.location = ''
errors.expiryDate = ''
serverError.value = '' serverError.value = ''
} }
// Clear individual field errors when the user types // Clear individual field errors when the user types
watch(() => form.title, () => { errors.title = ''; serverError.value = '' }) watch(() => form.title, () => { errors.title = ''; serverError.value = '' })
watch(() => form.dateTime, () => { errors.dateTime = ''; serverError.value = '' }) watch(() => form.dateTime, () => { errors.dateTime = ''; serverError.value = '' })
watch(() => form.expiryDate, () => { errors.expiryDate = ''; serverError.value = '' })
watch(() => form.description, () => { serverError.value = '' }) watch(() => form.description, () => { serverError.value = '' })
watch(() => form.location, () => { serverError.value = '' }) watch(() => form.location, () => { serverError.value = '' })
@@ -153,14 +128,6 @@ function validate(): boolean {
valid = false valid = false
} }
if (!form.expiryDate) {
errors.expiryDate = 'Expiry date is required.'
valid = false
} else if (form.expiryDate <= (new Date().toISOString().split('T')[0] ?? '')) {
errors.expiryDate = 'Expiry date must be in the future.'
valid = false
}
return valid return valid
} }
@@ -186,7 +153,6 @@ async function handleSubmit() {
dateTime: dateTimeWithOffset, dateTime: dateTimeWithOffset,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
location: form.location.trim() || undefined, location: form.location.trim() || undefined,
expiryDate: form.expiryDate,
}, },
}) })
@@ -212,7 +178,6 @@ async function handleSubmit() {
organizerToken: data.organizerToken, organizerToken: data.organizerToken,
title: data.title, title: data.title,
dateTime: data.dateTime, dateTime: data.dateTime,
expiryDate: data.expiryDate,
}) })
router.push({ name: 'event', params: { eventToken: data.eventToken } }) router.push({ name: 'event', params: { eventToken: data.eventToken } })

View File

@@ -25,10 +25,6 @@
<!-- Loaded state --> <!-- Loaded state -->
<div v-else-if="state === 'loaded' && event" class="detail__content"> <div v-else-if="state === 'loaded' && event" class="detail__content">
<div v-if="event.expired" class="detail__banner detail__banner--expired" role="status">
This event has ended.
</div>
<h1 class="detail__title">{{ event.title }}</h1> <h1 class="detail__title">{{ event.title }}</h1>
<dl class="detail__meta"> <dl class="detail__meta">
@@ -74,9 +70,9 @@
</div> </div>
</div> </div>
<!-- RSVP bar (only for loaded, non-expired events) --> <!-- RSVP bar -->
<RsvpBar <RsvpBar
v-if="state === 'loaded' && event && !event.expired && !isOrganizer" v-if="state === 'loaded' && event && !isOrganizer"
:has-rsvp="!!rsvpName" :has-rsvp="!!rsvpName"
@open="sheetOpen = true" @open="sheetOpen = true"
/> />
@@ -412,12 +408,6 @@ onMounted(fetchEvent)
text-align: center; text-align: center;
} }
.detail__banner--expired {
background: var(--color-glass);
color: var(--color-text-soft);
backdrop-filter: blur(4px);
}
/* Error / not-found message */ /* Error / not-found message */
.detail__message { .detail__message {
font-size: 1.1rem; font-size: 1.1rem;

View File

@@ -44,7 +44,6 @@ describe('EventCreateView', () => {
expect(wrapper.find('#description').exists()).toBe(true) expect(wrapper.find('#description').exists()).toBe(true)
expect(wrapper.find('#dateTime').exists()).toBe(true) expect(wrapper.find('#dateTime').exists()).toBe(true)
expect(wrapper.find('#location').exists()).toBe(true) expect(wrapper.find('#location').exists()).toBe(true)
expect(wrapper.find('#expiryDate').exists()).toBe(true)
}) })
it('has required attribute on required fields', async () => { it('has required attribute on required fields', async () => {
@@ -58,7 +57,6 @@ describe('EventCreateView', () => {
expect(wrapper.find('#title').attributes('required')).toBeDefined() expect(wrapper.find('#title').attributes('required')).toBeDefined()
expect(wrapper.find('#dateTime').attributes('required')).toBeDefined() expect(wrapper.find('#dateTime').attributes('required')).toBeDefined()
expect(wrapper.find('#expiryDate').attributes('required')).toBeDefined()
}) })
it('does not have required attribute on optional fields', async () => { it('does not have required attribute on optional fields', async () => {
@@ -102,7 +100,6 @@ describe('EventCreateView', () => {
// Fill required fields // Fill required fields
await wrapper.find('#title').setValue('My Event') await wrapper.find('#title').setValue('My Event')
await wrapper.find('#dateTime').setValue('2026-12-25T18:00') await wrapper.find('#dateTime').setValue('2026-12-25T18:00')
await wrapper.find('#expiryDate').setValue('2026-12-24')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
@@ -127,7 +124,7 @@ describe('EventCreateView', () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
const errorsBefore = wrapper.findAll('[role="alert"]').map((el) => el.text()).filter((t) => t.length > 0) const errorsBefore = wrapper.findAll('[role="alert"]').map((el) => el.text()).filter((t) => t.length > 0)
expect(errorsBefore.length).toBeGreaterThanOrEqual(3) expect(errorsBefore.length).toBeGreaterThanOrEqual(2)
// Type into title field // Type into title field
await wrapper.find('#title').setValue('My Event') await wrapper.find('#title').setValue('My Event')
@@ -138,9 +135,6 @@ describe('EventCreateView', () => {
const dateTimeError = wrapper.find('#dateTime').element.closest('.form-group')!.querySelector('[role="alert"]')! const dateTimeError = wrapper.find('#dateTime').element.closest('.form-group')!.querySelector('[role="alert"]')!
expect(dateTimeError.textContent).not.toBe('') expect(dateTimeError.textContent).not.toBe('')
const expiryError = wrapper.find('#expiryDate').element.closest('.form-group')!.querySelector('[role="alert"]')!
expect(expiryError.textContent).not.toBe('')
}) })
it('shows validation errors when submitting empty form', async () => { it('shows validation errors when submitting empty form', async () => {
@@ -156,7 +150,7 @@ describe('EventCreateView', () => {
const errorElements = wrapper.findAll('[role="alert"]') const errorElements = wrapper.findAll('[role="alert"]')
const errorTexts = errorElements.map((el) => el.text()).filter((t) => t.length > 0) const errorTexts = errorElements.map((el) => el.text()).filter((t) => t.length > 0)
expect(errorTexts.length).toBeGreaterThanOrEqual(3) expect(errorTexts.length).toBeGreaterThanOrEqual(2)
}) })
it('submits successfully, saves to storage, and navigates to event page', async () => { it('submits successfully, saves to storage, and navigates to event page', async () => {
@@ -179,7 +173,6 @@ describe('EventCreateView', () => {
title: 'Birthday Party', title: 'Birthday Party',
dateTime: '2026-12-25T18:00:00+01:00', dateTime: '2026-12-25T18:00:00+01:00',
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
expiryDate: '2026-12-24',
}, },
error: undefined, error: undefined,
response: new Response(), response: new Response(),
@@ -198,7 +191,6 @@ describe('EventCreateView', () => {
await wrapper.find('#description').setValue('Come celebrate!') await wrapper.find('#description').setValue('Come celebrate!')
await wrapper.find('#dateTime').setValue('2026-12-25T18:00') await wrapper.find('#dateTime').setValue('2026-12-25T18:00')
await wrapper.find('#location').setValue('Berlin') await wrapper.find('#location').setValue('Berlin')
await wrapper.find('#expiryDate').setValue('2026-12-24')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
@@ -208,7 +200,6 @@ describe('EventCreateView', () => {
title: 'Birthday Party', title: 'Birthday Party',
description: 'Come celebrate!', description: 'Come celebrate!',
location: 'Berlin', location: 'Berlin',
expiryDate: '2026-12-24',
}), }),
}) })
@@ -217,7 +208,6 @@ describe('EventCreateView', () => {
organizerToken: 'org-456', organizerToken: 'org-456',
title: 'Birthday Party', title: 'Birthday Party',
dateTime: '2026-12-25T18:00:00+01:00', dateTime: '2026-12-25T18:00:00+01:00',
expiryDate: '2026-12-24',
}) })
expect(pushSpy).toHaveBeenCalledWith({ expect(pushSpy).toHaveBeenCalledWith({
@@ -245,7 +235,6 @@ describe('EventCreateView', () => {
await wrapper.find('#title').setValue('Duplicate Event') await wrapper.find('#title').setValue('Duplicate Event')
await wrapper.find('#dateTime').setValue('2026-12-25T18:00') await wrapper.find('#dateTime').setValue('2026-12-25T18:00')
await wrapper.find('#expiryDate').setValue('2026-12-24')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
@@ -256,6 +245,5 @@ describe('EventCreateView', () => {
// Other field errors should not be present // Other field errors should not be present
expect(wrapper.find('#dateTime-error').exists()).toBe(false) expect(wrapper.find('#dateTime-error').exists()).toBe(false)
expect(wrapper.find('#expiryDate-error').exists()).toBe(false)
}) })
}) })

View File

@@ -54,7 +54,6 @@ const fullEvent = {
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
location: 'Central Park, NYC', location: 'Central Park, NYC',
attendeeCount: 12, attendeeCount: 12,
expired: false,
} }
function mockLoadedEvent(eventOverrides = {}) { function mockLoadedEvent(eventOverrides = {}) {
@@ -124,29 +123,6 @@ describe('EventDetailView', () => {
wrapper.unmount() wrapper.unmount()
}) })
// Expired state
it('renders "event has ended" banner when expired', async () => {
mockLoadedEvent({ expired: true })
const wrapper = await mountWithToken()
await flushPromises()
expect(wrapper.text()).toContain('This event has ended.')
expect(wrapper.find('.detail__banner--expired').exists()).toBe(true)
wrapper.unmount()
})
// No expired banner when not expired
it('does not render expired banner when event is active', async () => {
mockLoadedEvent()
const wrapper = await mountWithToken()
await flushPromises()
expect(wrapper.find('.detail__banner--expired').exists()).toBe(false)
wrapper.unmount()
})
// Not found state // Not found state
it('renders "event not found" when API returns 404', async () => { it('renders "event not found" when API returns 404', async () => {
vi.mocked(api.GET).mockResolvedValue({ vi.mocked(api.GET).mockResolvedValue({
@@ -229,17 +205,6 @@ describe('EventDetailView', () => {
wrapper.unmount() wrapper.unmount()
}) })
it('does not show RSVP bar on expired event', async () => {
mockLoadedEvent({ expired: true })
const wrapper = await mountWithToken()
await flushPromises()
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(false)
expect(wrapper.find('.rsvp-bar').exists()).toBe(false)
wrapper.unmount()
})
it('shows RSVP status bar when localStorage has RSVP', async () => { it('shows RSVP status bar when localStorage has RSVP', async () => {
mockGetRsvp.mockReturnValue({ rsvpToken: 'rsvp-1', rsvpName: 'Max' }) mockGetRsvp.mockReturnValue({ rsvpToken: 'rsvp-1', rsvpName: 'Max' })
mockLoadedEvent() mockLoadedEvent()