const assert = require('node:assert/strict');
const { test } = require('node:test');
const { once } = require('node:events');
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { spawn, spawnSync } = require('node:child_process');
const net = require('node:net');
const { WebSocketServer, WebSocket } = require('ws');
const { createApp, databasePath } = require('../dist/app');
const { DashboardStore } = require('../dist/store');
const { DashboardAuth, credentials, sessionToken } = require('../dist/auth');
const { PrivateCards } = require('../dist/cards');
const { live, connect } = require('./network.cjs');
const password = 'test-only-correct-password';
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fixture(t, options = {}) {
const directory = mkdtempSync(join(tmpdir(), 'redweb-private-cards-'));
const database = join(directory, 'cards.sqlite');
const store = new DashboardStore(database);
const secret = await credentials(password);
store.provision('alice', secret);
store.provision('bob', secret);
store.close();
let app;
t.after(async () => { await app?.shutdown(); rmSync(directory, { recursive: true, force: true }); });
async function restart() {
await app?.shutdown();
app = createApp({ port: 0, database, ...options });
if (!app.server.listening) await once(app.server, 'listening');
return `http://127.0.0.1:${app.server.address().port}`;
}
return { database, restart, origin: await restart(), get app() { return app; } };
}
function post(origin, path, values, cookie, suppliedOrigin = origin) {
return fetch(`${origin}${path}`, {
method: 'POST', redirect: 'manual',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: suppliedOrigin, ...(cookie ? { Cookie: cookie } : {}) },
body: new URLSearchParams(values),
});
}
async function login(origin, account = 'alice') {
const response = await post(origin, '/login', { account, password });
assert.equal(response.status, 303);
const header = response.headers.get('set-cookie');
assert.match(header, /HttpOnly; SameSite=Strict; Path=\//);
return header.split(';')[0];
}
async function cardClient(t, origin, cookie) {
const client = await live(t, origin, { Cookie: cookie });
const component = client.document.match(/data-rw-component="([^"]+)"/)[1];
await client.patch(patch => patch.id === 'root');
const parseCards = html => [...html.matchAll(/data-card-id="([^"]+)"[^>]*>
([\s\S]*?)<\/h2>/g)].map(match => ({
id: match[1], title: match[2].replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll(''', "'").replaceAll('&', '&'),
}));
return {
...client, component,
add: title => client.action('add', [{ title }], component),
remove: id => client.action('remove', [{ id }], component),
items: predicate => client.patch(patch => predicate(parseCards(patch.html))).then(message => parseCards(message.payload.patches.find(patch => predicate(parseCards(patch.html))).html)),
};
}
test('private live cards: real HTTP, sockets, isolation, reconnect, sign-out and durable restart', async t => {
const fixtureApp = await fixture(t);
let { origin } = fixtureApp;
assert.equal((await fetch(`${origin}/login`)).status, 200);
assert.equal((await fetch(origin)).status, 401);
assert.equal((await post(origin, '/login', { account: 'alice', password }, '', 'https://foreign.example')).status, 403);
assert.equal((await post(origin, '/login', { account: 'alice', password: 'wrong-password-at-least-16' })).status, 401);
const alice = await login(origin);
const alice2 = await login(origin);
const bob = await login(origin, 'bob');
const page = await fetch(origin, { headers: { Cookie: alice } });
assert.match(page.headers.get('cache-control'), /private.*no-store/);
assert.equal(page.headers.get('etag'), null);
const first = await cardClient(t, origin, alice);
const second = await cardClient(t, origin, alice2);
const other = await cardClient(t, origin, bob);
first.add('Saved ');
const [items] = await Promise.all([first.items(value => value.length === 1), second.items(value => value.length === 1)]);
assert.equal(items[0].title, 'Saved ');
const db = new DashboardStore(fixtureApp.database);
assert.deepEqual(db.list(sessionToken(bob)), []);
other.remove(items[0].id);
await delay(50);
assert.equal(db.list(sessionToken(alice)).length, 1);
first.action('add', [{ title: 'forged', account: 'bob' }], first.component);
const invalid = await first.receive(message => message.type === 'error');
assert.equal(invalid.error.code, 'ACTION_INVALID_INPUT');
assert.equal(db.list(sessionToken(alice)).length, 1);
const closed = once(first.socket, 'close'); first.socket.close(); await closed;
second.add('While disconnected');
await second.items(value => value.length === 2);
const config = first.config;
const reconnect = await connect(t, `${origin.replace('http:', 'ws:')}${config.socketPath}?pageId=${config.pageId}&redwebVersion=${encodeURIComponent(config.version)}`, origin, { Cookie: alice });
await reconnect.receive(message => message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html.includes('While disconnected')));
const deniedLogout = await post(origin, '/logout', {}, alice, 'https://foreign.example');
assert.equal(deniedLogout.status, 403);
const aliceClosed = once(reconnect.socket, 'close');
const secondClosed = once(second.socket, 'close');
const loggedOut = await post(origin, '/logout', {}, alice);
assert.equal(loggedOut.status, 303);
assert.match(loggedOut.headers.get('set-cookie'), /Max-Age=0/);
await Promise.all([aliceClosed, secondClosed]);
assert.equal((await fetch(origin, { headers: { Cookie: alice2 } })).status, 401);
assert.equal(other.socket.readyState, 1);
assert.equal(db.session(sessionToken(alice)), undefined);
db.close();
origin = await fixtureApp.restart();
const renewed = await login(origin);
const restored = await fetch(origin, { headers: { Cookie: renewed } });
const text = await restored.text();
assert.match(text, /While disconnected/);
assert.match(text, /<script>/);
const client = await cardClient(t, origin, renewed);
client.remove(items[0].id);
await client.items(value => value.length === 1);
});
test('session expiry closes idle sockets and rejects later HTTP access', async t => {
const { origin } = await fixture(t, { sessionLifetimeMs: 1200 });
const cookie = await login(origin);
const client = await cardClient(t, origin, cookie);
const [code] = await once(client.socket, 'close');
assert.equal(code, 1008);
assert.equal((await fetch(origin, { headers: { Cookie: cookie } })).status, 401);
});
test('store and authentication units use actual SQLite and scrypt, never substitutes', async t => {
const previousDatabase = process.env.DASHBOARD_DATABASE;
const previousPort = process.env.PORT;
try {
delete process.env.DASHBOARD_DATABASE;
delete process.env.PORT;
assert.equal(databasePath(), require('node:path').resolve('data/dashboard.sqlite'));
assert.throws(() => createApp({ origin: 'ftp://invalid.example' }), /exact/);
} finally {
if (previousDatabase === undefined) delete process.env.DASHBOARD_DATABASE; else process.env.DASHBOARD_DATABASE = previousDatabase;
if (previousPort === undefined) delete process.env.PORT; else process.env.PORT = previousPort;
}
const directory = mkdtempSync(join(tmpdir(), 'redweb-store-'));
const database = join(directory, 'unit.sqlite');
const store = new DashboardStore(database);
t.after(() => { store.close(); rmSync(directory, { recursive: true, force: true }); });
await assert.rejects(credentials('short'), /16/);
const secret = await credentials(password);
assert.throws(() => store.provision('?', secret), /Invalid/);
store.provision('alice', secret);
assert.throws(() => store.provision('alice', secret));
assert.equal(store.credentials('missing'), undefined);
assert.throws(() => store.issue('alice', 0));
const auth = new DashboardAuth(store);
assert.equal(await auth.login('peer', 'unknown', password), undefined);
assert.equal(await auth.login('peer', {}, password), undefined);
const token = await auth.login('peer', 'alice', password);
assert.equal(store.session(token).account, 'alice');
assert.equal(sessionToken(`redweb_dashboard=${token}`), token);
assert.equal(sessionToken(`redweb_dashboard=${token}; redweb_dashboard=${token}`), '');
assert.equal(sessionToken('redweb_dashboard=invalid'), '');
assert.equal(store.session('invalid'), undefined);
assert.throws(() => store.list('invalid'), /expired/);
assert.throws(() => store.add(token, ''), /Invalid/);
assert.throws(() => store.add(token, '\u0000'), /Invalid/);
for (let i = 0; i < 100; i++) store.add(token, `Card ${i}`);
assert.throws(() => store.add(token, 'Over capacity'), /limit/);
assert.equal(store.list(token).length, 100);
store.remove(token, store.list(token)[0].id);
assert.equal(store.list(token).length, 99);
for (let i = 1; i < 32; i++) store.issue('alice', 10000);
assert.throws(() => store.issue('alice', 10000), /existing sessions/);
assert.equal(store.signOut(token), 'alice');
assert.equal(store.signOut(token), undefined);
assert.throws(() => store.remove(token, 'anything'), /expired/);
for (let i = 0; i < 10; i++) await auth.login('limited', '!', password);
assert.equal(await auth.login('limited', 'alice', password), undefined);
store.close(); store.close();
const raw = new DatabaseSync(database);
raw.exec('PRAGMA user_version = 2'); raw.close();
assert.throws(() => new DashboardStore(database), /Unsupported/);
});
test('logout fences password checks in flight; close and admission bounds stop new sessions', async t => {
const store = new DashboardStore(':memory:');
t.after(() => store.close());
store.provision('alice', await credentials(password));
const auth = new DashboardAuth(store);
const token = store.issue('alice', 5000);
const pending = auth.login('peer', 'alice', password);
store.signOut(token);
await assert.rejects(pending, /Sign-out occurred/);
const closing = auth.login('peer', 'alice', password);
auth.close();
assert.equal(await closing, undefined);
assert.equal(await auth.login('peer', 'alice', password), undefined);
assert.throws(() => new DashboardAuth(store, 0));
const limited = new DashboardAuth(store);
const concurrent = Array.from({ length: 5 }, (_, index) => limited.login(`peer-${index}`, 'alice', password));
assert.equal(await concurrent[4], undefined);
const issued = await Promise.all(concurrent.slice(0, 4));
assert.ok(issued.every(Boolean));
for (let index = 0; index < 1024; index++) await limited.login(`invalid-${index}`, null, null);
assert.equal(await limited.login('new-peer', 'alice', password), undefined);
const expiring = store.issue('alice', 100);
await delay(110);
assert.equal(store.session(expiring), undefined);
store.issue('alice', 1000); // Prunes expired rows during issuance.
});
test('subscription cleanup is idempotent across replacement groups and failed callbacks', async t => {
const store = new DashboardStore(':memory:');
store.provision('alice', await credentials(password));
const token = store.issue('alice', 5000);
const cards = new PrivateCards(store);
const sockets = new WebSocketServer({ port: 0, host: '127.0.0.1' });
await once(sockets, 'listening');
const peers = [];
t.after(async () => {
for (const peer of peers) peer.terminate();
for (const peer of sockets.clients) peer.terminate();
await new Promise(resolve => sockets.close(resolve)); store.close();
});
async function context() {
const accepted = once(sockets, 'connection');
const client = new WebSocket(`ws://127.0.0.1:${sockets.address().port}`); peers.push(client);
const opened = once(client, 'open');
const [socket] = await accepted;
await opened;
const controller = new AbortController();
return { controller, value: { principal: 'alice', signal: controller.signal, socket, request: { get: name => name === 'cookie' ? `redweb_dashboard=${token}` : undefined } } };
}
const original = await context();
const cleanup = cards.subscribe(original.value, () => {});
original.controller.abort();
const replacement = await context();
let updates = 0;
const release = cards.subscribe(replacement.value, () => updates++);
cleanup(); cleanup();
cards.publish(store.add(token, 'Replacement still registered'));
assert.equal(updates, 2);
const broken = await context();
let fail = false;
cards.subscribe(broken.value, () => { if (fail) throw new Error('Intentional consumer failure'); });
fail = true;
cards.publish(store.add(token, 'Failure isolation'));
assert.equal(updates, 3);
const failedInitial = await context();
assert.throws(() => cards.subscribe(failedInitial.value, () => { throw new Error('Initial callback failure'); }), /Initial/);
const invalid = await context(); invalid.controller.abort();
assert.throws(() => cards.subscribe(invalid.value, () => {}), /Sign in/);
store.signOut(token);
cards.publish('alice');
assert.equal(updates, 3);
assert.throws(() => cards.subscribe(replacement.value, () => {}), /Sign in/);
release(); cards.publish('missing');
});
test('incomplete HTTP uploads cannot keep shutdown or the database alive indefinitely', async t => {
const directory = mkdtempSync(join(tmpdir(), 'redweb-drain-'));
const database = join(directory, 'drain.sqlite');
let app;
t.after(async () => { await app?.shutdown(); rmSync(directory, { recursive: true, force: true }); });
assert.throws(() => createApp({ port: 0, database, sessionLifetimeMs: 0 }), /lifetime/);
assert.throws(() => createApp({ port: 0, database, origin: 'https://example.com/path' }), /exact/);
assert.throws(() => createApp({ port: 0, database, origin: 'ftp://example.com' }), /exact/);
app = createApp({ port: 0, database });
await once(app.server, 'listening');
const socket = net.connect(app.server.address().port, '127.0.0.1');
t.after(() => socket.destroy());
socket.on('error', () => {});
await once(socket, 'connect');
socket.write('POST /login HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 1000\r\n\r\naccount=');
await delay(30);
const started = Date.now();
await app.shutdown();
assert.ok(Date.now() - started < 2000);
const reopened = new DashboardStore(database); reopened.close();
});
test('SQLite commits survive abrupt process termination rather than only graceful shutdown', async t => {
const directory = mkdtempSync(join(tmpdir(), 'redweb-crash-'));
const database = join(directory, 'crash.sqlite');
const store = new DashboardStore(database);
store.provision('alice', await credentials(password));
const token = store.issue('alice', 60000); store.close();
const child = spawn(process.execPath, ['-e', `
const { DashboardStore } = require('./dist/store');
const db = new DashboardStore(process.argv[1]);
db.add(process.argv[2], 'Committed before crash');
process.send('committed');
setInterval(() => {}, 1000);
`, database, token], { stdio: ['ignore', 'ignore', 'ignore', 'ipc'], windowsHide: true });
t.after(async () => {
if (child.exitCode === null && child.signalCode === null) { const exited = once(child, 'exit'); child.kill('SIGKILL'); await exited; }
rmSync(directory, { recursive: true, force: true });
});
assert.deepEqual(await once(child, 'message'), ['committed', undefined]);
const exited = once(child, 'exit'); child.kill('SIGKILL'); await exited;
const recovered = new DashboardStore(database);
try { assert.equal(recovered.list(token)[0].title, 'Committed before crash'); }
finally { recovered.close(); }
});
test('production origin/cookies and malformed forms use real HTTP', async t => {
const { origin } = await fixture(t, { origin: 'https://dashboard.example' });
const authenticated = await post(origin, '/login', { account: 'alice', password }, '', 'https://dashboard.example');
assert.equal(authenticated.status, 303);
assert.match(authenticated.headers.get('set-cookie'), /; Secure/);
assert.equal((await post(origin, '/login', { account: 'alice', password: 'x'.repeat(5000) })).status, 400);
assert.equal((await post(origin, '/logout', {}, '', 'https://dashboard.example')).status, 303);
assert.equal((await post(origin, '/login', {})).status, 403);
});
test('unit: listener-error cleanup observes rejection without hiding it from the application owner', async t => {
const directory = mkdtempSync(join(tmpdir(), 'redweb-dashboard-cleanup-'));
const database = join(directory, 'cards.sqlite');
const app = createApp({ port: 0, database });
t.after(async () => {
// This test deliberately makes the returned cleanup promise reject.
// Await settlement before removing files, including on assertion failure.
await Promise.allSettled([app.shutdown()]);
rmSync(directory, { recursive: true, force: true });
});
await once(app.server, 'listening');
const failure = new Error('Injected database cleanup failure');
const close = DashboardStore.prototype.close;
// Unit-only fault injection, not a claim of a naturally occurring SQLite
// failure. Real database/socket cleanup still runs; network ITs use no mocks.
const injected = t.mock.method(DashboardStore.prototype, 'close', function () {
close.call(this);
throw failure;
});
app.server.emit('error', new Error('Injected listener failure'));
const closing = app.shutdown();
assert.equal(app.shutdown(), closing);
await assert.rejects(closing, error => error === failure);
assert.equal(injected.mock.callCount(), 1);
assert.equal(app.server.listening, false);
injected.mock.restore();
const reopened = new DashboardStore(database);
reopened.close();
});
test('invalid-form middleware leaves an already destroyed native HTTP response untouched', async t => {
const { origin, app } = await fixture(t);
const handled = new Promise(resolve => app.server.once('request', (request, response) => resolve({ request, response })));
const page = await fetch(`${origin}/login`);
assert.equal(page.status, 200);
await page.text();
const { request, response } = await handled;
// Unit-test the defensive state with genuine Express objects. This is not
// a claim that an aborted upload naturally reaches this middleware branch.
const handlers = app.server.listeners('request').flatMap(listener => listener._router?.stack ?? [])
.filter(layer => layer.handle.name === 'invalidBody');
assert.equal(handlers.length, 1);
response.destroy();
assert.equal(response.destroyed, true);
const before = { status: response.statusCode, headers: response.getHeaders(), ended: response.writableEnded };
handlers[0].handle(new Error('Invalid form after disconnect'), request, response, () => assert.fail('must not forward'));
assert.deepEqual({ status: response.statusCode, headers: response.getHeaders(), ended: response.writableEnded }, before);
});
test('capacity failures and abandoned uploads remain contained over real HTTP', { timeout: 10000 }, async t => {
const { origin, database } = await fixture(t);
const store = new DashboardStore(database);
try { for (let index = 0; index < 32; index++) store.issue('alice', 60000); }
finally { store.close(); }
const response = await post(origin, '/login', { account: 'alice', password });
assert.equal(response.status, 503);
assert.equal(await response.text(), 'Unable to complete the request. Try again later.');
const socket = net.connect(Number(new URL(origin).port), '127.0.0.1');
socket.on('error', () => {});
t.after(() => socket.destroy());
await once(socket, 'connect');
const closed = new Promise(resolve => socket.once('close', resolve));
socket.end('POST /login HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 100\r\n\r\naccount=alice');
socket.resume();
await closed;
const reset = net.connect(Number(new URL(origin).port), '127.0.0.1');
reset.on('error', () => {});
t.after(() => reset.destroy());
await once(reset, 'connect');
const resetClosed = new Promise(resolve => reset.once('close', resolve));
reset.write('POST /login HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Encoding: gzip\r\nContent-Length: 100\r\n\r\n');
await delay(20);
reset.resetAndDestroy();
await resetClosed;
assert.equal((await fetch(`${origin}/login`)).status, 200);
});
test('real administrator and standalone startup commands expose errors and persist accounts', async t => {
const directory = mkdtempSync(join(tmpdir(), 'redweb-dashboard-cli-'));
const database = join(directory, 'cli.sqlite');
let app;
let child;
t.after(async () => {
if (child && child.exitCode === null && child.signalCode === null) { const exit = once(child, 'exit'); child.kill(); await exit; }
await app?.shutdown();
rmSync(directory, { recursive: true, force: true });
});
const env = { ...process.env, DASHBOARD_DATABASE: database, PORT: '0', NODE_ENV: 'test' };
delete env.DASHBOARD_ORIGIN;
const run = (file, args = [], overrides = {}) => spawnSync(process.execPath, [file, ...args], {
env: { ...env, ...overrides }, encoding: 'utf8', timeout: 10000, windowsHide: true,
});
assert.equal(run('dist/admin.js').status, 1);
assert.equal(run('dist/admin.js', ['?', 'extra']).status, 1);
const created = run('dist/admin.js', ['carol']);
assert.equal(created.status, 0); // Never include stdout (a generated password) in diagnostic output.
assert.ok(created.stdout.startsWith('Created carol.'));
assert.equal(run('dist/admin.js', ['carol']).status, 1);
assert.equal(run('dist/app.js', [], { NODE_ENV: 'production' }).status, 1);
assert.equal(run('dist/app.js', [], { NODE_ENV: 'production', DASHBOARD_ORIGIN: 'http://example.com' }).status, 1);
const store = new DashboardStore(database);
try { assert.ok(store.credentials('carol')); }
finally { store.close(); }
app = createApp({ database, port: 0 });
await once(app.server, 'listening');
const unavailable = run('dist/app.js', [], { PORT: String(app.server.address().port) });
assert.equal(unavailable.status, 1);
assert.match(unavailable.stderr, /Application listener failed/);
// Windows kill('SIGTERM') terminates immediately without invoking Node handlers.
// An actual IPC message delivers the signal event there; Unix uses its OS signal.
const signalControl = join(directory, 'signal.cjs');
writeFileSync(signalControl, "process.once('message', () => { process.disconnect(); process.emit('SIGTERM'); });");
for (const configured of [false, true]) {
const args = [...(process.platform === 'win32' ? ['--require', signalControl] : []), 'dist/app.js'];
child = spawn(process.execPath, args, { env: { ...env, ...(configured ? { DASHBOARD_ORIGIN: 'https://dashboard.example', NODE_ENV: 'production' } : {}) },
stdio: ['ignore', 'pipe', 'pipe', ...(process.platform === 'win32' ? ['ipc'] : [])], windowsHide: true });
let output = '', errors = '';
child.stdout.on('data', chunk => { output += chunk; });
child.stderr.on('data', chunk => { errors += chunk; });
const deadline = Date.now() + 5000;
while (!output.includes('/login') && Date.now() < deadline && child.exitCode === null) await delay(20);
assert.match(output, configured ? /Dashboard: https:\/\/dashboard.example\/login/ : /Dashboard: http:\/\/127\.0\.0\.1:\d+\/login/);
if (!configured) assert.equal((await fetch(output.match(/http:\/\/127\.0\.0\.1:\d+\/login/)[0])).status, 200);
const exit = once(child, 'exit');
if (process.platform === 'win32') child.send('stop');
else child.kill('SIGTERM');
const [code, signal] = await exit;
assert.equal(code, 0, errors);
assert.equal(signal, null);
}
});