Fix identified issues with server consolidation
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// Phase 9 §9.4 — vitest scaffold + auth-boundary tests.
|
||||
//
|
||||
// Covers shared/auth/middleware.js. Mocks the Postgres pool with a thin
|
||||
// in-memory fake — no real DB required.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticate, requirePermission } from './middleware.js';
|
||||
|
||||
const SECRET = 'test-secret-please-do-not-use-in-prod';
|
||||
|
||||
function makeFakePool(users, permissions = {}) {
|
||||
const calls = { count: 0 };
|
||||
return {
|
||||
calls,
|
||||
query: vi.fn(async (sql, params) => {
|
||||
calls.count += 1;
|
||||
if (sql.includes('FROM users WHERE id')) {
|
||||
const user = users[params[0]];
|
||||
return { rows: user ? [user] : [] };
|
||||
}
|
||||
if (sql.includes('FROM permissions')) {
|
||||
return { rows: (permissions[params[0]] || []).map((code) => ({ code })) };
|
||||
}
|
||||
return { rows: [] };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeReq(authHeader) {
|
||||
return { headers: authHeader ? { authorization: authHeader } : {} };
|
||||
}
|
||||
|
||||
function makeRes() {
|
||||
const res = {};
|
||||
res.status = vi.fn(() => res);
|
||||
res.json = vi.fn(() => res);
|
||||
return res;
|
||||
}
|
||||
|
||||
describe('authenticate middleware', () => {
|
||||
let activeUser;
|
||||
let inactiveUser;
|
||||
let validToken;
|
||||
|
||||
beforeEach(() => {
|
||||
activeUser = { id: 1, username: 'alice', email: 'a@x', is_admin: false, is_active: true };
|
||||
inactiveUser = { id: 2, username: 'bob', email: 'b@x', is_admin: false, is_active: false };
|
||||
validToken = jwt.sign({ userId: 1 }, SECRET, { expiresIn: '1h' });
|
||||
});
|
||||
|
||||
it('returns 401 when no Authorization header is present', async () => {
|
||||
const pool = makeFakePool({ 1: activeUser });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
await mw(makeReq(), res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 401 when Authorization is not Bearer', async () => {
|
||||
const pool = makeFakePool({ 1: activeUser });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
await mw(makeReq('Basic abc'), res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 401 when token is malformed', async () => {
|
||||
const pool = makeFakePool({ 1: activeUser });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
await mw(makeReq('Bearer not-a-jwt'), res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 403 when the user is inactive', async () => {
|
||||
const inactiveToken = jwt.sign({ userId: 2 }, SECRET, { expiresIn: '1h' });
|
||||
const pool = makeFakePool({ 2: inactiveUser });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
await mw(makeReq(`Bearer ${inactiveToken}`), res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls next() and populates req.user for a valid token + active user', async () => {
|
||||
const pool = makeFakePool({ 1: activeUser }, { 1: ['products:read'] });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const req = makeReq(`Bearer ${validToken}`);
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
await mw(req, res, next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.user.id).toBe(1);
|
||||
expect(req.user.permissions).toEqual(['products:read']);
|
||||
});
|
||||
|
||||
it('caches the user lookup — same token within TTL → one DB hit', async () => {
|
||||
const pool = makeFakePool({ 1: activeUser }, { 1: [] });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const next = vi.fn();
|
||||
await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
|
||||
await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
|
||||
// Two queries on first hit (user + permissions), zero on the second
|
||||
expect(pool.calls.count).toBe(2);
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('refetches after TTL expiry', async () => {
|
||||
vi.useFakeTimers();
|
||||
const pool = makeFakePool({ 1: activeUser }, { 1: [] });
|
||||
const mw = authenticate({ pool, secret: SECRET });
|
||||
const next = vi.fn();
|
||||
await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
|
||||
expect(pool.calls.count).toBe(2);
|
||||
vi.advanceTimersByTime(61_000);
|
||||
await mw(makeReq(`Bearer ${validToken}`), makeRes(), next);
|
||||
expect(pool.calls.count).toBe(4);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('requirePermission middleware', () => {
|
||||
it('returns 401 when req.user is missing', () => {
|
||||
const mw = requirePermission('products:write');
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
mw({}, res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls next() for admin users regardless of code', () => {
|
||||
const mw = requirePermission('products:write');
|
||||
const next = vi.fn();
|
||||
mw({ user: { is_admin: true, permissions: [] } }, makeRes(), next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls next() when user has the required permission', () => {
|
||||
const mw = requirePermission('products:write');
|
||||
const next = vi.fn();
|
||||
mw({ user: { is_admin: false, permissions: ['products:write'] } }, makeRes(), next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns 403 when user lacks the required permission', () => {
|
||||
const mw = requirePermission('products:write');
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
mw({ user: { is_admin: false, permissions: ['products:read'] } }, res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user