← back to AbramsOS
tests/auth-e2e.test.js
148 lines
// End-to-end: signup → enroll TOTP → signin (password+totp) → /import gate → step-up → /import 200.
// Talks to a fresh in-process server.
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const { authenticator } = require('otplib');
require('dotenv').config();
// Wipe + reset relevant tables so the test is reproducible
const { pool } = require('../lib/db');
test.before(async () => {
await pool.query(`DELETE FROM auth_event`);
await pool.query(`DELETE FROM auth_session`);
await pool.query(`DELETE FROM auth_totp`);
await pool.query(`DELETE FROM auth_credential`);
});
const app = require('../server');
let server;
test.before(() => new Promise((r) => { server = app.listen(0, r); }));
test.after(async () => {
await new Promise((r) => server.close(r));
await pool.end();
});
let cookieJar = {};
let csrfToken = '';
function cookieHeader() {
return Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
}
function captureCookie(res) {
const set = res.headers['set-cookie'];
if (set) {
for (const c of set) {
const m = c.match(/^([^=]+)=([^;]*)/);
if (!m) continue;
const name = m[1], val = m[2];
if (val === '') delete cookieJar[name];
else cookieJar[name] = val;
if (name === 'aos.csrf') csrfToken = val;
}
}
}
function req(path, { method = 'GET', body = null } = {}) {
return new Promise((resolve, reject) => {
const port = server.address().port;
const opts = {
method, hostname: '127.0.0.1', port, path,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookieHeader(),
'Accept': 'text/html,application/json',
},
};
const r = http.request(opts, (res) => {
let data = ''; res.on('data', (c) => (data += c));
res.on('end', () => {
captureCookie(res);
resolve({ status: res.statusCode, body: data, headers: res.headers });
});
});
r.on('error', reject);
// Auto-attach CSRF token for unsafe methods even when caller passed no body
const isUnsafe = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method);
if (isUnsafe && !body) body = {};
if (body) {
if (typeof body === 'object' && csrfToken && !body._csrf) body._csrf = csrfToken;
const enc = typeof body === 'string' ? body : new URLSearchParams(body).toString();
r.write(enc);
}
r.end();
});
}
// Prime CSRF cookie before any POST tests
test.before(async () => { await req('/signup'); });
test('signup creates user + sets cookie', async () => {
const r = await req('/signup', { method: 'POST', body: { email: 'steve+e2e@example.com', password: 'verystrongpassword!' } });
assert.strictEqual(r.status, 302);
assert.match(r.headers.location, /\/enroll-totp/);
assert.ok(cookieJar, 'session cookie should be set');
});
test('enroll-totp page shows the QR + secret', async () => {
const r = await req('/enroll-totp');
assert.strictEqual(r.status, 200);
assert.match(r.body, /Enroll two-factor authentication/);
});
test('TOTP enrollment with valid code completes', async () => {
const sec = (await pool.query(`SELECT secret_encrypted, secret_iv, secret_tag FROM auth_totp`)).rows[0];
const { decrypt } = require('../lib/crypto');
const secret = decrypt(sec.secret_encrypted, sec.secret_iv, sec.secret_tag);
const token = authenticator.generate(secret);
const r = await req('/enroll-totp', { method: 'POST', body: { token } });
assert.strictEqual(r.status, 302);
assert.match(r.headers.location, /\//);
});
test('after enrollment, / returns 200', async () => {
const r = await req('/');
assert.strictEqual(r.status, 200);
});
test('after enrollment with valid step-up, /import returns 200', async () => {
const r = await req('/import');
// step-up just happened during TOTP enroll → within 60s window → should pass
assert.strictEqual(r.status, 200);
assert.match(r.body, /Import all receipts now/);
});
test('signout clears cookie + /import becomes 302 again', async () => {
await req('/signout', { method: 'POST' });
const r = await req('/import');
assert.strictEqual(r.status, 302);
});
test('full signin (password → totp) restores access', async () => {
// password
const r1 = await req('/signin', { method: 'POST', body: { stage: 'password', email: 'steve+e2e@example.com', password: 'verystrongpassword!', next: '/' } });
assert.strictEqual(r1.status, 200);
assert.match(r1.body, /Two-factor code/);
// totp
const sec = (await pool.query(`SELECT secret_encrypted, secret_iv, secret_tag FROM auth_totp`)).rows[0];
const { decrypt } = require('../lib/crypto');
const secret = decrypt(sec.secret_encrypted, sec.secret_iv, sec.secret_tag);
const token = authenticator.generate(secret);
const r2 = await req('/signin', { method: 'POST', body: { stage: 'totp', token, next: '/' } });
assert.strictEqual(r2.status, 302);
assert.match(r2.headers.location, /\//);
const r3 = await req('/');
assert.strictEqual(r3.status, 200);
});
test('signup is locked after first user', async () => {
const r = await req('/signup');
assert.strictEqual(r.status, 302);
assert.match(r.headers.location, /\/signin/);
});