Files
inventory/inventory-server/shared/auth/middleware.test.js
T

239 lines
8.7 KiB
JavaScript

// 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('KIOSK_IPS bypass', () => {
it('bypasses token check and mints a synthetic kiosk user when req.ip matches', async () => {
const pool = makeFakePool({});
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
const req = { headers: {}, ip: '203.0.113.7' };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user).toEqual({
id: 'kiosk',
username: 'kiosk',
is_admin: false,
is_active: true,
permissions: [],
is_kiosk: true,
});
expect(pool.calls.count).toBe(0);
});
it('falls through to normal Bearer auth when req.ip is not in KIOSK_IPS', async () => {
const pool = makeFakePool({ 1: activeUser }, { 1: [] });
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
const req = { headers: { authorization: `Bearer ${validToken}` }, ip: '198.51.100.1' };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user.id).toBe(1);
expect(req.user.is_kiosk).toBeUndefined();
});
it('does NOT bypass when a Bearer token is present, even from a kiosk IP', async () => {
// A real user logged in from the same NAT'd network as the kiosk must
// keep their actual identity — otherwise the bypass silently strips
// their permissions and they 403 on gated routes.
const pool = makeFakePool({ 1: activeUser }, { 1: ['product_import'] });
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
const req = {
headers: { authorization: `Bearer ${validToken}` },
ip: '203.0.113.7',
};
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user.id).toBe(1);
expect(req.user.is_kiosk).toBeUndefined();
expect(req.user.permissions).toEqual(['product_import']);
});
it('does not bypass when KIOSK_IPS is empty, even if req.ip is undefined', async () => {
const pool = makeFakePool({ 1: activeUser });
const mw = authenticate({ pool, secret: SECRET, kioskIps: '' });
const req = { headers: {} };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
it('supports multiple comma-separated IPs', async () => {
const pool = makeFakePool({});
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7, 203.0.113.8 ,203.0.113.9' });
const next = vi.fn();
for (const ip of ['203.0.113.7', '203.0.113.8', '203.0.113.9']) {
const req = { headers: {}, ip };
await mw(req, makeRes(), next);
expect(req.user?.is_kiosk).toBe(true);
}
expect(next).toHaveBeenCalledTimes(3);
});
});
});
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();
});
});