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-cards/index.js'; import itemHandler from '../../pages/api/custom-cards/[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('GET /api/custom-cards', () => { 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('returns the user designs', async () => { const design = { id: 5, name: 'Emberwing', card_id: 77 }; sql.mockResolvedValueOnce({ rows: [design] }); const res = createRes(); await handler({ method: 'GET' }, res); expect(res.statusCode).toBe(200); expect(res.body.designs).toEqual([design]); }); }); describe('POST /api/custom-cards', () => { beforeEach(() => { vi.clearAllMocks(); getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' }); sql.mockResolvedValue({ rows: [] }); }); it('rejects designs without a name', async () => { const res = createRes(); await handler( { method: 'POST', body: { name: ' ', rarity: 'rare' } }, res ); expect(res.statusCode).toBe(400); expect(sql).not.toHaveBeenCalled(); }); it('creates the design, mirrors a catalog card, and links ownership', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 10, card_id: null, name: 'Emberwing Phoenix' }], }) // INSERT custom_cards .mockResolvedValueOnce({ rows: [{ id: 77 }] }) // INSERT cards twin .mockResolvedValueOnce({ rows: [] }) // INSERT user_cards .mockResolvedValueOnce({ rows: [{ id: 10, card_id: 77, name: 'Emberwing Phoenix' }], }); // link card_id back onto the design const res = createRes(); await handler( { method: 'POST', body: { name: 'Emberwing Phoenix', mana_cost: '2RR', rarity: 'rare', frameId: 'classic', description: 'A phoenix reborn.', actions: 'Flying, haste', }, }, res ); expect(res.statusCode).toBe(201); expect(res.body.design.card_id).toBe(77); // 4 statements: insert design, insert catalog twin, own row, link back expect(sql).toHaveBeenCalledTimes(4); }); }); describe('/api/custom-cards/[id]', () => { beforeEach(() => { vi.clearAllMocks(); getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' }); sql.mockResolvedValue({ rows: [] }); }); it('returns 404 when the design belongs to someone else', async () => { sql.mockResolvedValueOnce({ rows: [] }); // ownership lookup misses const res = createRes(); await itemHandler({ method: 'GET', query: { id: '12' } }, res); expect(res.statusCode).toBe(404); }); it('rejects invalid ids', async () => { const res = createRes(); await itemHandler({ method: 'GET', query: { id: 'abc' } }, res); expect(res.statusCode).toBe(400); }); it('updates the design and keeps the catalog twin in sync', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 10, user_id: 1, card_id: 77, name: 'Old Name' }], }) // ownership lookup hits .mockResolvedValueOnce({ rows: [{ id: 10, card_id: 77, name: 'New Name' }], }) // UPDATE custom_cards .mockResolvedValueOnce({ rows: [{ id: 77 }] }) // UPDATE cards twin .mockResolvedValueOnce({ rows: [] }); // ensureOwnedRow (no-op if exists) const res = createRes(); await itemHandler( { method: 'PUT', query: { id: '10' }, body: { name: 'New Name' } }, res ); expect(res.statusCode).toBe(200); expect(res.body.design.name).toBe('New Name'); // 4 statements: ownership, update design, update twin, own row expect(sql).toHaveBeenCalledTimes(4); }); it('deletes only the design row', async () => { sql .mockResolvedValueOnce({ rows: [{ id: 10, user_id: 1, card_id: 77 }], }) // ownership lookup .mockResolvedValueOnce({ rows: [] }); // DELETE custom_cards const res = createRes(); await itemHandler({ method: 'DELETE', query: { id: '10' } }, res); expect(res.statusCode).toBe(200); expect(sql).toHaveBeenCalledTimes(2); }); });