dragonflight/services/mam-api/test/middleware/auth.test.js

175 lines
7.7 KiB
JavaScript

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(); }
});
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);
});