import { beforeEach, describe, expect, it, vi } from 'vitest'; import jwt from 'jsonwebtoken'; vi.mock('../../lib/sql.js', () => ({ sql: vi.fn() })); import { sql } from '../../lib/sql.js'; import { JWT_SECRET } from '../../lib/auth-secret.js'; import { getUserFromRequest, withAdmin } from '../../lib/permission-middleware.js'; function makeToken(payload, opts = {}) { return jwt.sign(payload, JWT_SECRET, { expiresIn: opts.expiresIn ?? '1h' }); } describe('getUserFromRequest', () => { beforeEach(() => { sql.mockReset(); sql.mockResolvedValue({ rows: [] }); vi.spyOn(console, 'error').mockImplementation(() => {}); }); it('returns null when the Authorization header is missing', async () => { const user = await getUserFromRequest({ headers: {} }); expect(user).toBeNull(); expect(sql).not.toHaveBeenCalled(); }); it('returns null when the Authorization header is not a Bearer scheme', async () => { const user = await getUserFromRequest({ headers: { authorization: 'Basic foo' }, }); expect(user).toBeNull(); expect(sql).not.toHaveBeenCalled(); }); it('returns null when the token is malformed', async () => { const user = await getUserFromRequest({ headers: { authorization: 'Bearer not-a-jwt' }, }); expect(user).toBeNull(); expect(sql).not.toHaveBeenCalled(); }); it('returns null when the token signature uses a wrong secret', async () => { const token = jwt.sign({ userId: 1 }, 'other-secret', { expiresIn: '1h' }); const user = await getUserFromRequest({ headers: { authorization: `Bearer ${token}` }, }); expect(user).toBeNull(); expect(sql).not.toHaveBeenCalled(); }); it('returns null when the token is expired', async () => { const token = makeToken({ userId: 1 }, { expiresIn: '-1s' }); const user = await getUserFromRequest({ headers: { authorization: `Bearer ${token}` }, }); expect(user).toBeNull(); expect(sql).not.toHaveBeenCalled(); }); it('returns null when the token is valid but no user row matches', async () => { sql.mockResolvedValue({ rows: [] }); const token = makeToken({ userId: 42 }); const user = await getUserFromRequest({ headers: { authorization: `Bearer ${token}` }, }); expect(user).toBeNull(); expect(sql).toHaveBeenCalledTimes(1); }); it('returns the user object when the token is valid and the user row exists', async () => { sql.mockResolvedValue({ rows: [{ id: 42, email: 'a@b.c', role: 'user' }], }); const token = makeToken({ userId: 42 }); const user = await getUserFromRequest({ headers: { authorization: `Bearer ${token}` }, }); expect(user).toEqual({ userId: 42, email: 'a@b.c', role: 'user' }); expect(sql).toHaveBeenCalledTimes(1); }); it('does NOT return the synthetic admin shape when no Authorization header is present (Brief 2 regression lock)', async () => { const user = await getUserFromRequest({ headers: {} }); // Email literal is the OLD `admin@tcgvault.com` (pre-`pick-a-name` // convoy, 2026-05-24) — preserved as the exact pre-fix-auth-bypass // synthetic-admin shape this assertion locks against. The // `.toBeNull()` check below is the strong contract; this soft check // documents the historical bug. Do NOT update to // `admin@deckhearth.com` — that would weaken the regression-lock to // a shape that never actually existed. expect(user).not.toEqual({ userId: 1, email: 'admin@tcgvault.com', role: 'admin', }); expect(user).toBeNull(); }); }); describe('withAdmin', () => { beforeEach(() => { sql.mockReset(); sql.mockResolvedValue({ rows: [] }); vi.spyOn(console, 'error').mockImplementation(() => {}); }); it('returns 401 when unauthenticated', async () => { const inner = vi.fn(); const res = { status: vi.fn().mockReturnThis(), json: vi.fn() }; await withAdmin(inner)({ headers: {} }, res); expect(res.status).toHaveBeenCalledWith(401); expect(inner).not.toHaveBeenCalled(); }); it('returns 403 when user is not admin', async () => { sql.mockResolvedValueOnce({ rows: [{ id: 2, email: 'alice@deckhearth.com', role: 'user' }], }); const token = makeToken({ userId: 2, email: 'alice@deckhearth.com' }); const inner = vi.fn(); const res = { status: vi.fn().mockReturnThis(), json: vi.fn() }; await withAdmin(inner)( { headers: { authorization: `Bearer ${token}` } }, res ); expect(res.status).toHaveBeenCalledWith(403); expect(inner).not.toHaveBeenCalled(); }); it('calls inner handler with admin user', async () => { sql.mockResolvedValueOnce({ rows: [{ id: 1, email: 'admin@deckhearth.com', role: 'admin' }], }); const token = makeToken({ userId: 1, email: 'admin@deckhearth.com' }); const inner = vi.fn().mockResolvedValue(undefined); const req = { headers: { authorization: `Bearer ${token}` } }; const res = {}; await withAdmin(inner)(req, res); expect(inner).toHaveBeenCalledWith(req, res, { userId: 1, email: 'admin@deckhearth.com', role: 'admin', }); }); });