test(03-05): add failing tests for write client calls and eventForm store keys
- createEvent: POST /api/events/create with credentials:include, returns uid - updateEvent: PATCH /api/events/:uid/edit - fetchWritableCalendars: GET /api/events/writable-calendars, returns calendars array - calendarStore: eventFormOpen, eventFormMode, eventFormUid defaults and setEventForm setter
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* client.ts unit tests — Task 1 (TDD RED → GREEN)
|
||||
*
|
||||
* Tests for write client calls added in Plan 03-05:
|
||||
* - createEvent: POSTs to /api/events/create with credentials:'include'
|
||||
* - updateEvent: PATCHes /api/events/:uid/edit
|
||||
* - fetchWritableCalendars: GETs /api/events/writable-calendars, returns WritableCalendar[]
|
||||
* Tests for Zustand store new keys:
|
||||
* - eventFormOpen: boolean, defaults false
|
||||
* - eventFormMode: 'create'|'edit', defaults 'create'
|
||||
* - eventFormUid: string|null, defaults null
|
||||
* - setEventForm: setter for all three
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
// ── createEvent tests ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('createEvent', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('POSTs to /api/events/create with credentials:include', async () => {
|
||||
const mockFetch = vi.mocked(fetch)
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ uid: 'test-uid-123' }),
|
||||
} as Response)
|
||||
|
||||
const { createEvent } = await import('./client.js')
|
||||
const payload = {
|
||||
title: 'Team Meeting',
|
||||
allDay: false,
|
||||
start: '2026-06-15T10:00:00',
|
||||
end: '2026-06-15T11:00:00',
|
||||
recurrence: 'none' as const,
|
||||
}
|
||||
await createEvent(payload)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/api/events/create',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns { uid } from 202 response', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ uid: 'returned-uid-456' }),
|
||||
} as Response)
|
||||
|
||||
const { createEvent } = await import('./client.js')
|
||||
const result = await createEvent({
|
||||
title: 'Test',
|
||||
allDay: true,
|
||||
start: '2026-06-20',
|
||||
end: '2026-06-20',
|
||||
recurrence: 'none' as const,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ uid: 'returned-uid-456' })
|
||||
})
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({ error: 'Bad Request' }),
|
||||
} as Response)
|
||||
|
||||
const { createEvent } = await import('./client.js')
|
||||
await expect(
|
||||
createEvent({
|
||||
title: '',
|
||||
allDay: false,
|
||||
start: '2026-06-15T10:00:00',
|
||||
end: '2026-06-15T11:00:00',
|
||||
recurrence: 'none' as const,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ── updateEvent tests ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('updateEvent', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('PATCHes /api/events/:uid/edit', async () => {
|
||||
const mockFetch = vi.mocked(fetch)
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ uid: 'edit-uid-789' }),
|
||||
} as Response)
|
||||
|
||||
const { updateEvent } = await import('./client.js')
|
||||
await updateEvent('edit-uid-789', {
|
||||
title: 'Updated Meeting',
|
||||
allDay: false,
|
||||
start: '2026-06-15T10:00:00',
|
||||
end: '2026-06-15T11:00:00',
|
||||
recurrence: 'weekly' as const,
|
||||
})
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/api/events/edit-uid-789/edit',
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns { uid } on success', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ uid: 'patched-uid' }),
|
||||
} as Response)
|
||||
|
||||
const { updateEvent } = await import('./client.js')
|
||||
const result = await updateEvent('some-uid', {
|
||||
title: 'Patched',
|
||||
allDay: false,
|
||||
start: '2026-06-15T10:00:00',
|
||||
end: '2026-06-15T11:00:00',
|
||||
recurrence: 'none' as const,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ uid: 'patched-uid' })
|
||||
})
|
||||
})
|
||||
|
||||
// ── fetchWritableCalendars tests ──────────────────────────────────────────────
|
||||
|
||||
describe('fetchWritableCalendars', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('GETs /api/events/writable-calendars with credentials:include', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
calendars: [
|
||||
{ url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false },
|
||||
],
|
||||
}),
|
||||
} as Response)
|
||||
|
||||
const { fetchWritableCalendars } = await import('./client.js')
|
||||
await fetchWritableCalendars()
|
||||
|
||||
expect(vi.mocked(fetch)).toHaveBeenCalledWith(
|
||||
'/api/events/writable-calendars',
|
||||
expect.objectContaining({ credentials: 'include' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns WritableCalendar[] from the calendars envelope', async () => {
|
||||
const mockCalendars = [
|
||||
{ url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false },
|
||||
{ url: 'https://caldav.fastmail.com/cal2', displayName: 'Family', color: '#F25C7A', isShared: true },
|
||||
]
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ calendars: mockCalendars }),
|
||||
} as Response)
|
||||
|
||||
const { fetchWritableCalendars } = await import('./client.js')
|
||||
const result = await fetchWritableCalendars()
|
||||
|
||||
expect(result).toEqual(mockCalendars)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
} as Response)
|
||||
|
||||
const { fetchWritableCalendars } = await import('./client.js')
|
||||
await expect(fetchWritableCalendars()).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Zustand store new keys tests ──────────────────────────────────────────────
|
||||
|
||||
describe('calendarStore — eventForm keys', () => {
|
||||
it('eventFormOpen defaults to false', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('eventFormMode defaults to create', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormMode).toBe('create')
|
||||
})
|
||||
|
||||
it('eventFormUid defaults to null', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormUid).toBeNull()
|
||||
})
|
||||
|
||||
it('setEventForm(true, edit, some-uid) updates all three keys', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
useCalendarStore.getState().setEventForm(true, 'edit', 'some-uid')
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormOpen).toBe(true)
|
||||
expect(state.eventFormMode).toBe('edit')
|
||||
expect(state.eventFormUid).toBe('some-uid')
|
||||
})
|
||||
|
||||
it('setEventForm(false) closes the form and preserves other defaults', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
// Open first
|
||||
useCalendarStore.getState().setEventForm(true, 'edit', 'uid-abc')
|
||||
// Close
|
||||
useCalendarStore.getState().setEventForm(false)
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('setEventForm(true) with no mode/uid defaults to create/null', async () => {
|
||||
const { useCalendarStore } = await import('../store/calendarStore.js')
|
||||
useCalendarStore.getState().setEventForm(true)
|
||||
const state = useCalendarStore.getState()
|
||||
expect(state.eventFormOpen).toBe(true)
|
||||
expect(state.eventFormMode).toBe('create')
|
||||
expect(state.eventFormUid).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user