import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../../lib/sql.js', () => ({ sql: vi.fn() })); vi.mock('../../lib/permission-middleware', () => ({ getUserFromRequest: vi.fn(), })); import { sql } from '../../lib/sql.js'; import { getUserFromRequest } from '../../lib/permission-middleware'; import handler from '../../pages/api/custom-games/index.js'; import itemHandler from '../../pages/api/custom-games/[id].js'; function createRes() { const res = { statusCode: 200, body: null, status(code) { res.statusCode = code; return res; }, json(data) { res.body = data; return res; }, }; return res; } describe('/api/custom-games', () => { beforeEach(() => { vi.clearAllMocks(); getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' }); sql.mockResolvedValue({ rows: [] }); }); it('requires authentication', async () => { getUserFromRequest.mockResolvedValue(null); const res = createRes(); await handler({ method: 'GET' }, res); expect(res.statusCode).toBe(401); expect(sql).not.toHaveBeenCalled(); }); it('rejects games without a name', async () => { const res = createRes(); await handler({ method: 'POST', body: { name: ' ' } }, res); expect(res.statusCode).toBe(400); expect(sql).not.toHaveBeenCalled(); }); it('rejects duplicate names for the same user', async () => { sql.mockResolvedValueOnce({ rows: [{ id: 9 }] }); // duplicate lookup hits const res = createRes(); await handler({ method: 'POST', body: { name: 'Aetherfall' } }, res); expect(res.statusCode).toBe(409); }); it('creates a game scoped to the user', async () => { sql .mockResolvedValueOnce({ rows: [] }) // duplicate lookup misses .mockResolvedValueOnce({ rows: [{ id: 9, name: 'Aetherfall', description: null }], }); const res = createRes(); await handler( { method: 'POST', body: { name: 'Aetherfall', description: 'Skyborn TCG' } }, res ); expect(res.statusCode).toBe(201); expect(res.body.game.name).toBe('Aetherfall'); expect(res.body.game.card_count).toBe(0); }); }); describe('/api/custom-games/[id]', () => { beforeEach(() => { vi.clearAllMocks(); getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' }); sql.mockResolvedValue({ rows: [] }); }); it('returns 404 for another user\'s game', async () => { sql.mockResolvedValueOnce({ rows: [] }); // ownership lookup misses const res = createRes(); await itemHandler({ method: 'GET', query: { id: '9' } }, res); expect(res.statusCode).toBe(404); }); it('returns the game with its designs', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 9, name: 'Aetherfall' }] }) // ownership .mockResolvedValueOnce({ rows: [{ id: 3, name: 'Stormsage' }] }); // designs const res = createRes(); await itemHandler({ method: 'GET', query: { id: '9' } }, res); expect(res.statusCode).toBe(200); expect(res.body.game.name).toBe('Aetherfall'); expect(res.body.designs).toEqual([{ id: 3, name: 'Stormsage' }]); }); it('rejects renames that clash with another game', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 9, name: 'Old Name' }] }) // ownership .mockResolvedValueOnce({ rows: [{ id: 12 }] }); // clash lookup hits const res = createRes(); await itemHandler( { method: 'PUT', query: { id: '9' }, body: { name: 'Taken' } }, res ); expect(res.statusCode).toBe(409); }); it('deletes the game row', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 9, name: 'Aetherfall' }] }) // ownership .mockResolvedValueOnce({ rows: [] }); // DELETE const res = createRes(); await itemHandler({ method: 'DELETE', query: { id: '9' } }, res); expect(res.statusCode).toBe(200); expect(sql).toHaveBeenCalledTimes(2); }); });