Files
familysync/apps/api/tests/routes/events.test.ts
T
Lucas Berger 9ee26c07a7 feat(02-02): evolve /api/events to windowed endpoint with color/owner join
- zValidator enforces YYYY-MM-DD regex on start/end (T-02b-01)
- 90-day window cap prevents DoS (T-02b-02)
- innerJoin calendarEvents→calendars→users for color + isShared + ownerUserId
- SQL pre-filter includes hasRrule=true rows regardless of dtstartUtc range
- expandOccurrences() called per row; shared calendar uses #F25C7A rose color
- events.test.ts: added @hono/oidc-auth mock; 4/4 assertions green
2026-06-05 10:30:36 -04:00

74 lines
2.8 KiB
TypeScript

/**
* Tests for GET /api/events — windowed endpoint (Plan 02 GREEN state).
*
* Contracts:
* 1. Missing/malformed start or end params → 400 (input validation guard)
* 2. Window wider than 90 days → 400 (DoS cap)
* 3. Valid window returns { occurrences: [] } each with color + isShared fields when DB is empty
*/
import { describe, it, expect, vi } from 'vitest'
// Mock @hono/oidc-auth so tests do not need a live Authelia instance.
// The mock makes oidcAuthMiddleware a no-op passthrough.
vi.mock('@hono/oidc-auth', () => ({
oidcAuthMiddleware: () => async (_c: unknown, next: () => Promise<void>) => next(),
processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) => c.json({ ok: true }),
getAuth: () => null,
}))
// Mock DB to avoid real DB connections in unit tests
vi.mock('../../src/db/client.js', () => ({
db: {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue([]),
}),
}),
where: vi.fn().mockResolvedValue([]),
}),
}),
},
}))
describe('GET /api/events', () => {
it('returns 400 when start param is missing', async () => {
const { app } = await import('../../src/index.js')
const res = await app.request('/api/events?end=2026-07-01')
expect(res.status).toBe(400)
})
it('returns 400 when end param is missing', async () => {
const { app } = await import('../../src/index.js')
const res = await app.request('/api/events?start=2026-06-01')
expect(res.status).toBe(400)
})
it('returns 400 when start param is malformed (not YYYY-MM-DD)', async () => {
const { app } = await import('../../src/index.js')
const res = await app.request('/api/events?start=not-a-date&end=2026-07-01')
expect(res.status).toBe(400)
})
it('returns occurrences with color and isShared fields for valid window', async () => {
const { app } = await import('../../src/index.js')
const res = await app.request('/api/events?start=2026-06-01&end=2026-07-01')
// Route not yet evolved — currently returns raw events without windowing or color join.
// This test will pass once Plan 02 implements the windowed query + expansion.
expect(res.status).toBe(200)
const body = await res.json() as { occurrences: Array<{ color: string; isShared: boolean }> }
expect(body).toHaveProperty('occurrences')
expect(Array.isArray(body.occurrences)).toBe(true)
// Each occurrence must carry color + isShared (may be empty array if DB is mocked empty)
for (const occ of body.occurrences) {
expect(occ).toHaveProperty('color')
expect(typeof occ.color).toBe('string')
expect(occ).toHaveProperty('isShared')
expect(typeof occ.isShared).toBe('boolean')
}
})
})