deckhearth/test/api/custom-games.test.js
Randall Stillwell 5b9a278ca2 feat(designer): inline symbol icons, frame textures, community sharing, print sheets
- {CODE} tokens in description/actions/flavor render as inline symbol
  icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
  remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
  game space, public listing at /community/games, read-only game view,
  /api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
  (63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
2026-08-24 21:48:52 -05:00

137 lines
3.9 KiB
JavaScript

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', custom_frame: null }]);
});
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);
});
});