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>
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import { http, HttpResponse } from 'msw'
|
|
import { test, expect } from './msw-setup'
|
|
import type { StoredEvent } from '../src/composables/useEventStorage'
|
|
|
|
const STORAGE_KEY = 'fete:events'
|
|
|
|
const fullEvent = {
|
|
eventToken: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
title: 'Summer BBQ',
|
|
description: 'Bring your own drinks!',
|
|
dateTime: '2026-03-15T20:00:00+01:00',
|
|
timezone: 'Europe/Berlin',
|
|
location: 'Central Park, NYC',
|
|
attendeeCount: 12,
|
|
cancelled: false,
|
|
cancellationReason: null,
|
|
}
|
|
|
|
const organizerToken = '550e8400-e29b-41d4-a716-446655440001'
|
|
|
|
function seedEvents(events: StoredEvent[]): string {
|
|
return `window.localStorage.setItem('${STORAGE_KEY}', ${JSON.stringify(JSON.stringify(events))})`
|
|
}
|
|
|
|
function organizerSeed(): StoredEvent {
|
|
return {
|
|
eventToken: fullEvent.eventToken,
|
|
organizerToken,
|
|
title: fullEvent.title,
|
|
dateTime: fullEvent.dateTime,
|
|
}
|
|
}
|
|
|
|
test.describe('US1: Organizer cancels event with reason', () => {
|
|
test('organizer opens cancel bottom sheet, enters reason, confirms — event shows as cancelled on reload', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
let cancelled = false
|
|
network.use(
|
|
http.get('*/api/events/:token', () => {
|
|
if (cancelled) {
|
|
return HttpResponse.json({
|
|
...fullEvent,
|
|
cancelled: true,
|
|
cancellationReason: 'Venue closed',
|
|
})
|
|
}
|
|
return HttpResponse.json(fullEvent)
|
|
}),
|
|
http.patch('*/api/events/:token', ({ request }) => {
|
|
const url = new URL(request.url)
|
|
const token = url.searchParams.get('organizerToken')
|
|
if (token === organizerToken) {
|
|
cancelled = true
|
|
return new HttpResponse(null, { status: 204 })
|
|
}
|
|
return HttpResponse.json(
|
|
{ type: 'urn:problem-type:invalid-organizer-token', title: 'Forbidden', status: 403 },
|
|
{ status: 403 },
|
|
)
|
|
}),
|
|
)
|
|
await page.addInitScript(seedEvents([organizerSeed()]))
|
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
|
|
|
// Cancel button visible for organizer
|
|
const cancelBtn = page.getByRole('button', { name: /Cancel event/i })
|
|
await expect(cancelBtn).toBeVisible()
|
|
|
|
// Open cancel bottom sheet
|
|
await cancelBtn.click()
|
|
|
|
// Fill in reason
|
|
const reasonField = page.getByLabel(/reason/i)
|
|
await expect(reasonField).toBeVisible()
|
|
await reasonField.fill('Venue closed')
|
|
|
|
// Confirm cancellation
|
|
await page.getByRole('button', { name: /Confirm cancellation/i }).click()
|
|
|
|
// Event should show as cancelled
|
|
await expect(page.getByText(/This event has been cancelled/i)).toBeVisible()
|
|
await expect(page.getByText('Venue closed')).toBeVisible()
|
|
|
|
// Cancel button should be gone
|
|
await expect(cancelBtn).not.toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('US1: Organizer cancels event without reason', () => {
|
|
test('organizer cancels without reason — event shows as cancelled', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
let cancelled = false
|
|
network.use(
|
|
http.get('*/api/events/:token', () => {
|
|
if (cancelled) {
|
|
return HttpResponse.json({
|
|
...fullEvent,
|
|
cancelled: true,
|
|
cancellationReason: null,
|
|
})
|
|
}
|
|
return HttpResponse.json(fullEvent)
|
|
}),
|
|
http.patch('*/api/events/:token', ({ request }) => {
|
|
const url = new URL(request.url)
|
|
const token = url.searchParams.get('organizerToken')
|
|
if (token === organizerToken) {
|
|
cancelled = true
|
|
return new HttpResponse(null, { status: 204 })
|
|
}
|
|
return HttpResponse.json({}, { status: 403 })
|
|
}),
|
|
)
|
|
await page.addInitScript(seedEvents([organizerSeed()]))
|
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
|
|
|
await page.getByRole('button', { name: /Cancel event/i }).click()
|
|
|
|
// Don't fill in reason, just confirm
|
|
await page.getByRole('button', { name: /Confirm cancellation/i }).click()
|
|
|
|
// Event should show as cancelled without reason text
|
|
await expect(page.getByText(/This event has been cancelled/i)).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('US1: Cancel API failure', () => {
|
|
test('cancel API fails — error displayed in bottom sheet, button re-enabled for retry', async ({
|
|
page,
|
|
network,
|
|
}) => {
|
|
network.use(
|
|
http.get('*/api/events/:token', () => HttpResponse.json(fullEvent)),
|
|
http.patch('*/api/events/:token', () => {
|
|
return HttpResponse.json(
|
|
{
|
|
type: 'about:blank',
|
|
title: 'Internal Server Error',
|
|
status: 500,
|
|
detail: 'Something went wrong',
|
|
},
|
|
{ status: 500, headers: { 'Content-Type': 'application/problem+json' } },
|
|
)
|
|
}),
|
|
)
|
|
await page.addInitScript(seedEvents([organizerSeed()]))
|
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
|
|
|
await page.getByRole('button', { name: /Cancel event/i }).click()
|
|
await page.getByRole('button', { name: /Confirm cancellation/i }).click()
|
|
|
|
// Error message in bottom sheet
|
|
await expect(page.getByText(/Could not cancel event/i)).toBeVisible()
|
|
|
|
// Confirm button should be re-enabled
|
|
await expect(page.getByRole('button', { name: /Confirm cancellation/i })).toBeEnabled()
|
|
})
|
|
})
|