Implement cancel-event feature (016)

Add PATCH /events/{eventToken} endpoint for organizers to cancel events,
cancellation banner for visitors, and RSVP rejection on cancelled events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 19:52:22 +01:00
parent 3908c89998
commit 541017965f
20 changed files with 1004 additions and 106 deletions

View File

@@ -8,6 +8,7 @@ import de.fete.adapter.in.web.model.CreateRsvpRequest;
import de.fete.adapter.in.web.model.CreateRsvpResponse;
import de.fete.adapter.in.web.model.GetAttendeesResponse;
import de.fete.adapter.in.web.model.GetEventResponse;
import de.fete.adapter.in.web.model.PatchEventRequest;
import de.fete.application.service.EventNotFoundException;
import de.fete.application.service.InvalidTimezoneException;
import de.fete.domain.model.CreateEventCommand;
@@ -22,6 +23,7 @@ import de.fete.domain.port.in.CreateEventUseCase;
import de.fete.domain.port.in.CreateRsvpUseCase;
import de.fete.domain.port.in.GetAttendeesUseCase;
import de.fete.domain.port.in.GetEventUseCase;
import de.fete.domain.port.in.UpdateEventUseCase;
import java.time.DateTimeException;
import java.time.ZoneId;
import java.util.List;
@@ -40,6 +42,7 @@ public class EventController implements EventsApi {
private final CancelRsvpUseCase cancelRsvpUseCase;
private final CountAttendeesByEventUseCase countAttendeesByEventUseCase;
private final GetAttendeesUseCase getAttendeesUseCase;
private final UpdateEventUseCase updateEventUseCase;
/** Creates a new controller with the given use cases. */
public EventController(
@@ -48,13 +51,15 @@ public class EventController implements EventsApi {
CreateRsvpUseCase createRsvpUseCase,
CancelRsvpUseCase cancelRsvpUseCase,
CountAttendeesByEventUseCase countAttendeesByEventUseCase,
GetAttendeesUseCase getAttendeesUseCase) {
GetAttendeesUseCase getAttendeesUseCase,
UpdateEventUseCase updateEventUseCase) {
this.createEventUseCase = createEventUseCase;
this.getEventUseCase = getEventUseCase;
this.createRsvpUseCase = createRsvpUseCase;
this.cancelRsvpUseCase = cancelRsvpUseCase;
this.countAttendeesByEventUseCase = countAttendeesByEventUseCase;
this.getAttendeesUseCase = getAttendeesUseCase;
this.updateEventUseCase = updateEventUseCase;
}
@Override
@@ -97,10 +102,23 @@ public class EventController implements EventsApi {
response.setLocation(event.getLocation());
response.setAttendeeCount(
(int) countAttendeesByEventUseCase.countByEvent(evtToken));
response.setCancelled(event.isCancelled());
response.setCancellationReason(event.getCancellationReason());
return ResponseEntity.ok(response);
}
@Override
public ResponseEntity<Void> patchEvent(
UUID eventToken, UUID organizerToken, PatchEventRequest request) {
updateEventUseCase.cancelEvent(
new EventToken(eventToken),
new OrganizerToken(organizerToken),
request.getCancelled(),
request.getCancellationReason());
return ResponseEntity.noContent().build();
}
@Override
public ResponseEntity<GetAttendeesResponse> getAttendees(
UUID eventToken, UUID organizerToken) {

View File

@@ -1,5 +1,7 @@
package de.fete.adapter.in.web;
import de.fete.application.service.EventAlreadyCancelledException;
import de.fete.application.service.EventCancelledException;
import de.fete.application.service.EventExpiredException;
import de.fete.application.service.EventNotFoundException;
import de.fete.application.service.ExpiryDateBeforeEventException;
@@ -75,6 +77,32 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
.body(problemDetail);
}
/** Handles attempt to cancel an already cancelled event. */
@ExceptionHandler(EventAlreadyCancelledException.class)
public ResponseEntity<ProblemDetail> handleEventAlreadyCancelled(
EventAlreadyCancelledException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, ex.getMessage());
problemDetail.setTitle("Event Already Cancelled");
problemDetail.setType(URI.create("urn:problem-type:event-already-cancelled"));
return ResponseEntity.status(HttpStatus.CONFLICT)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problemDetail);
}
/** Handles RSVP on cancelled event. */
@ExceptionHandler(EventCancelledException.class)
public ResponseEntity<ProblemDetail> handleEventCancelled(
EventCancelledException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, ex.getMessage());
problemDetail.setTitle("Event Cancelled");
problemDetail.setType(URI.create("urn:problem-type:event-cancelled"));
return ResponseEntity.status(HttpStatus.CONFLICT)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problemDetail);
}
/** Handles RSVP on expired event. */
@ExceptionHandler(EventExpiredException.class)
public ResponseEntity<ProblemDetail> handleEventExpired(

View File

@@ -46,6 +46,12 @@ public class EventJpaEntity {
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@Column(name = "cancelled", nullable = false)
private boolean cancelled;
@Column(name = "cancellation_reason", length = 2000)
private String cancellationReason;
/** Returns the internal database ID. */
public Long getId() {
return id;
@@ -145,4 +151,24 @@ public class EventJpaEntity {
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
/** Returns whether the event is cancelled. */
public boolean isCancelled() {
return cancelled;
}
/** Sets the cancelled flag. */
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
/** Returns the cancellation reason. */
public String getCancellationReason() {
return cancellationReason;
}
/** Sets the cancellation reason. */
public void setCancellationReason(String cancellationReason) {
this.cancellationReason = cancellationReason;
}
}

View File

@@ -48,6 +48,8 @@ public class EventPersistenceAdapter implements EventRepository {
entity.setLocation(event.getLocation());
entity.setExpiryDate(event.getExpiryDate());
entity.setCreatedAt(event.getCreatedAt());
entity.setCancelled(event.isCancelled());
entity.setCancellationReason(event.getCancellationReason());
return entity;
}
@@ -63,6 +65,8 @@ public class EventPersistenceAdapter implements EventRepository {
event.setLocation(entity.getLocation());
event.setExpiryDate(entity.getExpiryDate());
event.setCreatedAt(entity.getCreatedAt());
event.setCancelled(entity.isCancelled());
event.setCancellationReason(entity.getCancellationReason());
return event;
}
}

View File

@@ -0,0 +1,12 @@
package de.fete.application.service;
import java.util.UUID;
/** Thrown when attempting to cancel an event that is already cancelled. */
public class EventAlreadyCancelledException extends RuntimeException {
/** Creates a new exception for the given event token. */
public EventAlreadyCancelledException(UUID eventToken) {
super("Event is already cancelled: " + eventToken);
}
}

View File

@@ -0,0 +1,12 @@
package de.fete.application.service;
import java.util.UUID;
/** Thrown when an RSVP is attempted on a cancelled event. */
public class EventCancelledException extends RuntimeException {
/** Creates a new exception for the given event token. */
public EventCancelledException(UUID eventToken) {
super("Event is cancelled: " + eventToken);
}
}

View File

@@ -6,16 +6,18 @@ import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken;
import de.fete.domain.port.in.CreateEventUseCase;
import de.fete.domain.port.in.GetEventUseCase;
import de.fete.domain.port.in.UpdateEventUseCase;
import de.fete.domain.port.out.EventRepository;
import java.time.Clock;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** Application service implementing event creation and retrieval. */
@Service
public class EventService implements CreateEventUseCase, GetEventUseCase {
public class EventService implements CreateEventUseCase, GetEventUseCase, UpdateEventUseCase {
private static final int EXPIRY_DAYS_AFTER_EVENT = 7;
@@ -50,4 +52,29 @@ public class EventService implements CreateEventUseCase, GetEventUseCase {
public Optional<Event> getByEventToken(EventToken eventToken) {
return eventRepository.findByEventToken(eventToken);
}
@Transactional
@Override
public void cancelEvent(
EventToken eventToken, OrganizerToken organizerToken,
Boolean cancelled, String reason) {
if (!Boolean.TRUE.equals(cancelled)) {
return;
}
Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
if (!event.getOrganizerToken().equals(organizerToken)) {
throw new InvalidOrganizerTokenException();
}
if (event.isCancelled()) {
throw new EventAlreadyCancelledException(eventToken.value());
}
event.setCancelled(true);
event.setCancellationReason(reason);
eventRepository.save(event);
}
}

View File

@@ -42,6 +42,10 @@ public class RsvpService
Event event = eventRepository.findByEventToken(eventToken)
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
if (event.isCancelled()) {
throw new EventCancelledException(eventToken.value());
}
if (!event.getExpiryDate().isAfter(LocalDate.now(clock))) {
throw new EventExpiredException(eventToken.value());
}

View File

@@ -17,6 +17,8 @@ public class Event {
private String location;
private LocalDate expiryDate;
private OffsetDateTime createdAt;
private boolean cancelled;
private String cancellationReason;
/** Returns the internal database ID. */
public Long getId() {
@@ -117,4 +119,24 @@ public class Event {
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
/** Returns whether the event has been cancelled. */
public boolean isCancelled() {
return cancelled;
}
/** Sets the cancelled flag. */
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
/** Returns the cancellation reason, if any. */
public String getCancellationReason() {
return cancellationReason;
}
/** Sets the cancellation reason. */
public void setCancellationReason(String cancellationReason) {
this.cancellationReason = cancellationReason;
}
}

View File

@@ -0,0 +1,13 @@
package de.fete.domain.port.in;
import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken;
/** Inbound port for updating an event. */
public interface UpdateEventUseCase {
/** Cancels the event identified by the given token. */
void cancelEvent(
EventToken eventToken, OrganizerToken organizerToken,
Boolean cancelled, String reason);
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="004-add-cancellation-columns" author="fete">
<addColumn tableName="events">
<column name="cancelled" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="cancellation_reason" type="VARCHAR(2000)"/>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -9,5 +9,6 @@
<include file="db/changelog/001-create-events-table.xml"/>
<include file="db/changelog/002-add-timezone-column.xml"/>
<include file="db/changelog/003-create-rsvps-table.xml"/>
<include file="db/changelog/004-add-cancellation-columns.xml"/>
</databaseChangeLog>

View File

@@ -184,6 +184,58 @@ paths:
schema:
$ref: "#/components/schemas/ProblemDetail"
patch:
operationId: patchEvent
summary: Update an event (currently cancel)
description: |
Partial update of an event resource. Currently the only supported operation
is cancellation (setting cancelled to true). Requires the organizer token.
Cancellation is irreversible.
tags:
- events
parameters:
- name: eventToken
in: path
required: true
schema:
type: string
format: uuid
description: Public event token
- name: organizerToken
in: query
required: true
schema:
type: string
format: uuid
description: Organizer token for authorization
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PatchEventRequest"
responses:
"204":
description: Event updated successfully
"403":
description: Invalid organizer token
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
"404":
description: Event not found
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
"409":
description: Event is already cancelled
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
components:
schemas:
CreateEventRequest:
@@ -252,6 +304,7 @@ components:
- dateTime
- timezone
- attendeeCount
- cancelled
properties:
eventToken:
type: string
@@ -284,6 +337,31 @@ components:
minimum: 0
description: Number of confirmed attendees (attending=true)
example: 12
cancelled:
type: boolean
description: Whether the event has been cancelled
example: false
cancellationReason:
type:
- string
- "null"
description: Reason for cancellation, if provided
example: null
PatchEventRequest:
type: object
required:
- cancelled
properties:
cancelled:
type: boolean
description: Set to true to cancel the event (irreversible)
example: true
cancellationReason:
type: string
maxLength: 2000
description: Optional cancellation reason
example: "Unfortunately the venue is no longer available."
CreateRsvpRequest:
type: object

View File

@@ -3,6 +3,7 @@ package de.fete.adapter.in.web;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -21,6 +22,7 @@ import de.fete.adapter.out.persistence.RsvpJpaRepository;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -431,6 +433,147 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.attendeeCount").value(1));
}
// --- Cancel Event tests ---
@Test
void cancelEventReturns204AndPersists() throws Exception {
EventJpaEntity event = seedEvent(
"Cancel Me", null, "Europe/Berlin", null, LocalDate.now().plusDays(30));
var body = Map.of(
"cancelled", true,
"cancellationReason", "Venue closed");
mockMvc.perform(patch("/api/events/" + event.getEventToken()
+ "?organizerToken=" + event.getOrganizerToken())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isNoContent());
EventJpaEntity persisted = jpaRepository
.findByEventToken(event.getEventToken()).orElseThrow();
assertThat(persisted.isCancelled()).isTrue();
assertThat(persisted.getCancellationReason()).isEqualTo("Venue closed");
}
@Test
void cancelEventWithoutReasonReturns204() throws Exception {
EventJpaEntity event = seedEvent(
"Cancel No Reason", null, "Europe/Berlin", null, LocalDate.now().plusDays(30));
var body = Map.of("cancelled", true);
mockMvc.perform(patch("/api/events/" + event.getEventToken()
+ "?organizerToken=" + event.getOrganizerToken())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isNoContent());
EventJpaEntity persisted = jpaRepository
.findByEventToken(event.getEventToken()).orElseThrow();
assertThat(persisted.isCancelled()).isTrue();
assertThat(persisted.getCancellationReason()).isNull();
}
@Test
void cancelEventWithWrongOrganizerTokenReturns403() throws Exception {
EventJpaEntity event = seedEvent(
"Wrong Token", null, "Europe/Berlin", null, LocalDate.now().plusDays(30));
var body = Map.of("cancelled", true);
mockMvc.perform(patch("/api/events/" + event.getEventToken()
+ "?organizerToken=" + UUID.randomUUID())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isForbidden())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:invalid-organizer-token"));
assertThat(jpaRepository.findByEventToken(event.getEventToken())
.orElseThrow().isCancelled()).isFalse();
}
@Test
void cancelEventNotFoundReturns404() throws Exception {
var body = Map.of("cancelled", true);
mockMvc.perform(patch("/api/events/" + UUID.randomUUID()
+ "?organizerToken=" + UUID.randomUUID())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isNotFound())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found"));
}
@Test
void cancelAlreadyCancelledEventReturns409() throws Exception {
EventJpaEntity event = seedCancelledEvent("Already Cancelled");
var body = Map.of("cancelled", true);
mockMvc.perform(patch("/api/events/" + event.getEventToken()
+ "?organizerToken=" + event.getOrganizerToken())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isConflict())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:event-already-cancelled"));
}
@Test
void getEventReturnsCancelledFields() throws Exception {
EventJpaEntity event = seedCancelledEvent("Weather Event");
mockMvc.perform(get("/api/events/" + event.getEventToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.cancelled").value(true))
.andExpect(jsonPath("$.cancellationReason").value("Cancelled"));
}
@Test
void getEventReturnsNotCancelledByDefault() throws Exception {
EventJpaEntity event = seedEvent(
"Active Event", null, "Europe/Berlin", null, LocalDate.now().plusDays(30));
mockMvc.perform(get("/api/events/" + event.getEventToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.cancelled").value(false))
.andExpect(jsonPath("$.cancellationReason").doesNotExist());
}
@Test
void createRsvpOnCancelledEventReturns409() throws Exception {
EventJpaEntity event = seedCancelledEvent("Cancelled RSVP");
long countBefore = rsvpJpaRepository.count();
var request = new CreateRsvpRequest().name("Late Guest");
mockMvc.perform(post("/api/events/" + event.getEventToken() + "/rsvps")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:event-cancelled"));
assertThat(rsvpJpaRepository.count()).isEqualTo(countBefore);
}
private EventJpaEntity seedCancelledEvent(String title) {
var entity = new EventJpaEntity();
entity.setEventToken(UUID.randomUUID());
entity.setOrganizerToken(UUID.randomUUID());
entity.setTitle(title);
entity.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
entity.setTimezone("Europe/Berlin");
entity.setExpiryDate(LocalDate.now().plusDays(30));
entity.setCreatedAt(OffsetDateTime.now());
entity.setCancelled(true);
entity.setCancellationReason("Cancelled");
return jpaRepository.save(entity);
}
private UUID seedRsvpAndGetToken(EventJpaEntity event, String name) {
var rsvp = new RsvpJpaEntity();
UUID token = UUID.randomUUID();

View File

@@ -0,0 +1,138 @@
package de.fete.application.service;
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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import de.fete.application.service.EventAlreadyCancelledException;
import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken;
import de.fete.domain.port.out.EventRepository;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class EventServiceCancelTest {
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
private static final Instant FIXED_INSTANT =
LocalDate.of(2026, 3, 5).atStartOfDay(ZONE).toInstant();
private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, ZONE);
@Mock
private EventRepository eventRepository;
private EventService eventService;
@BeforeEach
void setUp() {
eventService = new EventService(eventRepository, FIXED_CLOCK);
}
@Test
void cancelEventDelegatesToDomainAndSaves() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
eventService.cancelEvent(eventToken, organizerToken, true, "Venue unavailable");
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isEqualTo("Venue unavailable");
}
@Test
void cancelEventWithNullReason() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(false);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
eventService.cancelEvent(eventToken, organizerToken, true, null);
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
verify(eventRepository).save(captor.capture());
assertThat(captor.getValue().isCancelled()).isTrue();
assertThat(captor.getValue().getCancellationReason()).isNull();
}
@Test
void cancelEventThrows404WhenNotFound() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> eventService.cancelEvent(eventToken, organizerToken, true, null))
.isInstanceOf(EventNotFoundException.class);
verify(eventRepository, never()).save(any());
}
@Test
void cancelEventThrows403WhenWrongOrganizerToken() {
EventToken eventToken = EventToken.generate();
OrganizerToken correctToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(correctToken);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
final OrganizerToken wrongToken = OrganizerToken.generate();
assertThatThrownBy(() -> eventService.cancelEvent(eventToken, wrongToken, true, null))
.isInstanceOf(InvalidOrganizerTokenException.class);
verify(eventRepository, never()).save(any());
}
@Test
void cancelEventThrows409WhenAlreadyCancelled() {
EventToken eventToken = EventToken.generate();
OrganizerToken organizerToken = OrganizerToken.generate();
var event = new Event();
event.setEventToken(eventToken);
event.setOrganizerToken(organizerToken);
event.setCancelled(true);
when(eventRepository.findByEventToken(eventToken))
.thenReturn(Optional.of(event));
assertThatThrownBy(() -> eventService.cancelEvent(eventToken, organizerToken, true, null))
.isInstanceOf(EventAlreadyCancelledException.class);
verify(eventRepository, never()).save(any());
}
}