- game system selector: standalone, existing system (MTG/Pokemon/Lorcana/ SWU/FaB/One Piece/Sorcery/Grand Archive — codes match catalog imports), or a user's custom game - custom_games table + CRUD API (private per user, unique names) - /games hub with create form; /games/[id] space with rename, delete, card gallery, and ?game= deep-link into the designer - catalog twin resolves game: system code, custom game name, or 'Custom' - my-designs shows each design's game association - migration 1787693311000
137 lines
3.9 KiB
JavaScript
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' }]);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|