Merge pull request 'Make expiryDate an internal concern, auto-set to event date + 7 days' (#25) from auto-expiry-date into master
This commit was merged in pull request #25.
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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', () => ({
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } })
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user