import assert from 'node:assert/strict';
import { request as httpRequest } from 'node:http';
import test from 'node:test';
import { listenForCode, openBrowser } from '../src/browser-login.js';
test('listener binds loopback only and closes on timeout', async () => {
const lb = await listenForCode({ state: 's', timeoutMs: 20 });
assert.match(lb.redirectUri, /^http:\/\/127\.0\.0\.1:\d+\/$/);
await assert.rejects(lb.code, /timed out/);
lb.close();
await assert.rejects(fetch(lb.redirectUri), /fetch failed/);
});
test('matching state resolves the code', async () => {
const lb = await listenForCode({ state: 's', timeoutMs: 5000 });
const res = await fetch(`${lb.redirectUri}?code=c&state=s`);
assert.equal(res.status, 200);
assert.equal(await lb.code, 'c');
lb.close();
});
test('openBrowser reports a missing opener instead of throwing synchronously', async () => {
const orig = process.env.PATH;
process.env.PATH = '/nonexistent';
try {
await assert.rejects(openBrowser('https://x.example/?a=b&c=d', 'linux'), { code: 'ENOENT' });
} finally {
process.env.PATH = orig;
}
});
test('callback hardening: generic errors, safe headers, single params, origin-form only', async () => {
const lb = await listenForCode({ state: 's', timeoutMs: 5000 });
const base = lb.redirectUri;
const bad = async (q) => (await fetch(`${base}?${q}`)).status;
assert.equal(await bad('code=a&code=b&state=s'), 400);
assert.equal(await bad('code=a&state=s&state=s'), 400);
assert.equal(await bad('code=a&state=t'), 400);
const port = new URL(base).port;
const absolute = await new Promise((resolve) => {
const req = httpRequest(
{ port, host: '127.0.0.1', path: `http://evil.example/?code=a&state=s` },
(res) => (res.resume(), resolve(res.statusCode)),
);
req.end();
});
assert.equal(absolute, 404);
const res = await fetch(`${base}?error=SECRET-TOKEN-123&state=s`);
const body = await res.text();
assert.ok(!body.includes('SECRET'));
assert.match(body, /not granted|received/);
assert.equal(res.headers.get('cache-control'), 'no-store');
assert.equal(res.headers.get('referrer-policy'), 'no-referrer');
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
await assert.rejects(lb.code, (err) => {
assert.match(err.message, /\(access_denied\)/);
assert.ok(!err.message.includes('SECRET'));
return true;
});
lb.close();
});
test('abort during listener startup is not lost', async () => {
const controller = new AbortController();
const listening = listenForCode({ state: 's', timeoutMs: 5000, signal: controller.signal });
controller.abort();
await assert.rejects(listening, /cancelled/);
});