2026-05-27 13:59:50 -04:00
|
|
|
import { test } from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';
|
|
|
|
|
import { requireAuth, DEV_USER_ID } from '../../src/middleware/auth.js';
|
|
|
|
|
import { generateToken, hashToken } from '../../src/auth/tokens.js';
|
|
|
|
|
|
|
|
|
|
function mockRes() {
|
|
|
|
|
const res = {
|
|
|
|
|
statusCode: 200, body: null,
|
|
|
|
|
status(n) { this.statusCode = n; return this; },
|
|
|
|
|
json(o) { this.body = o; return this; },
|
|
|
|
|
};
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockReq({ session = null, authHeader = null } = {}) {
|
|
|
|
|
return {
|
|
|
|
|
session,
|
|
|
|
|
headers: authHeader ? { authorization: authHeader } : {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('AUTH_ENABLED=false → attaches dev user and calls next', async () => {
|
|
|
|
|
delete process.env.AUTH_ENABLED;
|
|
|
|
|
const req = mockReq(); const res = mockRes(); let called = false;
|
|
|
|
|
await requireAuth(req, res, () => { called = true; });
|
|
|
|
|
assert.equal(called, true);
|
|
|
|
|
assert.equal(req.user.id, DEV_USER_ID);
|
|
|
|
|
assert.equal(req.user.username, 'dev');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AUTH_ENABLED=true + no session + no bearer → 401', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?cache=' + Date.now());
|
|
|
|
|
const req = mockReq(); const res = mockRes();
|
|
|
|
|
await requireAuth(req, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
|
|
|
assert.deepEqual(res.body, { error: 'unauthorized' });
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('valid session within idle/absolute window → next + req.user populated', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
`INSERT INTO users (username, password_hash, display_name, role)
|
|
|
|
|
VALUES ('alice', 'x', 'Alice', 'admin') RETURNING id`);
|
|
|
|
|
const userId = rows[0].id;
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const req = mockReq({ session: { user_id: userId, first_seen_at: now - 1000, last_seen_at: now - 500 } });
|
|
|
|
|
const res = mockRes(); let called = false;
|
|
|
|
|
await requireAuth(req, res, () => { called = true; });
|
|
|
|
|
assert.equal(called, true, 'next not called; body=' + JSON.stringify(res.body));
|
|
|
|
|
assert.equal(req.user.id, userId);
|
|
|
|
|
assert.equal(req.user.username, 'alice');
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('idle-expired session (>1h since last_seen_at) → 401', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
`INSERT INTO users (username, password_hash) VALUES ('b', 'x') RETURNING id`);
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const session = { user_id: rows[0].id, first_seen_at: now - 1000, last_seen_at: now - (61 * 60 * 1000), destroy(cb){ cb(); } };
|
|
|
|
|
const req = mockReq({ session }); const res = mockRes();
|
|
|
|
|
await requireAuth(req, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('absolute-expired session (>8h since first_seen_at) → 401', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
const { rows } = await pool.query(
|
|
|
|
|
`INSERT INTO users (username, password_hash) VALUES ('c', 'x') RETURNING id`);
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const session = { user_id: rows[0].id, first_seen_at: now - (9 * 3600 * 1000), last_seen_at: now - 100, destroy(cb){ cb(); } };
|
|
|
|
|
const req = mockReq({ session }); const res = mockRes();
|
|
|
|
|
await requireAuth(req, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('valid bearer token → next + req.user populated', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
const { rows: u } = await pool.query(
|
|
|
|
|
`INSERT INTO users (username, password_hash) VALUES ('d', 'x') RETURNING id`);
|
|
|
|
|
const token = generateToken();
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO api_tokens (user_id, name, token_hash, token_prefix)
|
|
|
|
|
VALUES ($1, 'test', $2, $3)`,
|
|
|
|
|
[u[0].id, hashToken(token), token.slice(0, 8)]);
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const req = mockReq({ authHeader: 'Bearer ' + token });
|
|
|
|
|
const res = mockRes(); let called = false;
|
|
|
|
|
await requireAuth(req, res, () => { called = true; });
|
|
|
|
|
assert.equal(called, true, 'next not called; body=' + JSON.stringify(res.body));
|
|
|
|
|
assert.equal(req.user.username, 'd');
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('invalid bearer token → 401', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const req = mockReq({ authHeader: 'Bearer dfl_nope' });
|
|
|
|
|
const res = mockRes();
|
|
|
|
|
await requireAuth(req, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('bearer token whose user was deleted → 401', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
|
|
|
|
|
const pool = await setupTestDb();
|
|
|
|
|
process.env.AUTH_ENABLED = 'true';
|
|
|
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
|
|
|
const { rows: u } = await pool.query(
|
|
|
|
|
`INSERT INTO users (username, password_hash) VALUES ('e', 'x') RETURNING id`);
|
|
|
|
|
const token = generateToken();
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO api_tokens (user_id, name, token_hash, token_prefix)
|
|
|
|
|
VALUES ($1, 'test', $2, $3)`,
|
|
|
|
|
[u[0].id, hashToken(token), token.slice(0, 8)]);
|
|
|
|
|
await pool.query(`DELETE FROM users WHERE id = $1`, [u[0].id]);
|
|
|
|
|
try {
|
|
|
|
|
const { requireAuth } = await import('../../src/middleware/auth.js?v=' + Date.now());
|
|
|
|
|
const req = mockReq({ authHeader: 'Bearer ' + token });
|
|
|
|
|
const res = mockRes();
|
|
|
|
|
await requireAuth(req, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
|
|
|
} finally { await pool.end(); }
|
|
|
|
|
});
|
2026-05-27 14:58:02 -04:00
|
|
|
|
|
|
|
|
import { requireUiHeader } from '../../src/middleware/auth.js';
|
|
|
|
|
|
|
|
|
|
test('requireUiHeader: GET → next (any header)', () => {
|
|
|
|
|
let called = false;
|
|
|
|
|
requireUiHeader({ method: 'GET', headers: {} }, { status: () => ({ json: () => {} }) }, () => { called = true; });
|
|
|
|
|
assert.equal(called, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('requireUiHeader: POST without header → 403', () => {
|
|
|
|
|
const res = { status(n) { this.statusCode = n; return this; }, json(o) { this.body = o; } };
|
|
|
|
|
requireUiHeader({ method: 'POST', headers: {} }, res, () => {});
|
|
|
|
|
assert.equal(res.statusCode, 403);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('requireUiHeader: POST with correct header → next', () => {
|
|
|
|
|
let called = false;
|
|
|
|
|
requireUiHeader({ method: 'POST', headers: { 'x-requested-with': 'dragonflight-ui' } }, {}, () => { called = true; });
|
|
|
|
|
assert.equal(called, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('requireUiHeader: POST with bearer auth → next (exempt)', () => {
|
|
|
|
|
let called = false;
|
|
|
|
|
requireUiHeader({ method: 'POST', headers: { authorization: 'Bearer dfl_xxx' } }, {}, () => { called = true; });
|
|
|
|
|
assert.equal(called, true);
|
|
|
|
|
});
|