'use strict'; // QA-B1-01 — POST /login for a webauthn-enabled user must return the // requires_webauthn two-step payload (not fall through and 500), and a user // whose webauthn_enabled flag is stale (no credential rows) must still be able // to log in with their password instead of being locked out. const test = require('node:test'); const assert = require('node:assert/strict'); const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-webauthn-login-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database.cts'); const { hashPassword } = require('../services/authService.cts'); const router = require('../routes/auth.cts'); const PASSWORD = 'correct-horse-battery'; function handler(method, routePath) { const layer = router.stack.find((l) => l.route?.path === routePath && l.route.methods[method]); assert.ok(layer, `${method.toUpperCase()} ${routePath} exists`); return layer.route.stack[layer.route.stack.length - 1].handle; } function callLogin(body) { const h = handler('post', '/login'); return new Promise((resolve, reject) => { const req = { body, ip: '127.0.0.1', cookies: {}, headers: {}, secure: false, get: () => 'node-test-agent', }; const cookies = []; const res = { statusCode: 200, status(code) { this.statusCode = code; return this; }, cookie(name, value, opts) { cookies.push({ name, value, opts }); }, json(data) { resolve({ status: this.statusCode, body: data, cookies }); }, }; Promise.resolve(h(req, res)).catch(reject); }); } let webauthnUserId; let staleFlagUserId; test.before(async () => { const db = getDb(); const hash = await hashPassword(PASSWORD); const u1 = db .prepare( "INSERT INTO users (username, password_hash, role, webauthn_enabled) VALUES (?, ?, 'user', 1)", ) .run('webauthn-user', hash); webauthnUserId = u1.lastInsertRowid; db.prepare( 'INSERT INTO webauthn_credentials (user_id, credential_id, public_key, sign_count, transports) VALUES (?, ?, ?, 0, ?)', ).run(webauthnUserId, 'dGVzdC1jcmVkLWlk', 'dGVzdC1wdWJsaWMta2V5', JSON.stringify(['internal'])); const u2 = db .prepare( "INSERT INTO users (username, password_hash, role, webauthn_enabled) VALUES (?, ?, 'user', 1)", ) .run('stale-flag-user', hash); staleFlagUserId = u2.lastInsertRowid; }); test.after(() => { closeDb(); for (const suffix of ['', '-shm', '-wal']) { try { fs.unlinkSync(dbPath + suffix); } catch {} } }); test('webauthn-enabled user gets the two-step payload, not a 500', async () => { const { status, body, cookies } = await callLogin({ username: 'webauthn-user', password: PASSWORD, }); assert.equal(status, 200); assert.equal(body.requires_webauthn, true); assert.equal(typeof body.challenge_token, 'string'); assert.ok(body.challenge_token.length > 0); assert.ok(body.webauthn_options, 'webauthn_options present'); assert.equal(typeof body.webauthn_options.challenge, 'string'); assert.equal(cookies.length, 0, 'no session cookie before the key verifies'); assert.equal(body.user, undefined, 'no user payload before the key verifies'); const challenge = getDb() .prepare( "SELECT id FROM webauthn_challenges WHERE user_id = ? AND challenge_type = 'authentication'", ) .get(webauthnUserId); assert.ok(challenge, 'authentication challenge persisted'); }); test('stale webauthn flag (no credentials) falls through to password login, not lockout', async () => { const { status, body, cookies } = await callLogin({ username: 'stale-flag-user', password: PASSWORD, }); assert.equal(status, 200); assert.equal(body.requires_webauthn, undefined); assert.ok(body.user, 'password login succeeded despite the stale flag'); assert.equal(body.user.id, staleFlagUserId); assert.equal(cookies.length, 1, 'session cookie issued'); }); test('wrong password still 401s for a webauthn-enabled user', async () => { const { status, body } = await callLogin({ username: 'webauthn-user', password: 'nope-wrong' }); assert.equal(status, 401); assert.equal(body.code, 'AUTH_ERROR'); });