import assert from "node:assert/strict"; import { test } from "node:test"; import { pickDecoys, pickNumber } from "../src/numbers.ts"; import { SessionStore, secretEquals } from "../src/sessions.ts"; const opts = { ttlSeconds: 90, numberMode: "type" as const, maxSessions: 10 }; const newStore = (o = {}) => new SessionStore({ ...opts, ...o }); test("a fresh session carries two DIFFERENT secrets", () => { const s = newStore().create(0, "10.0.0.1", "ua"); // The poll id is visible on the laptop's screen; the approval token is only // in the QR. Reusing one value for both would collapse that separation. assert.notEqual(s.id, s.approvalToken); assert.equal(s.status, "pending"); }); test("the happy path approves and yields the username exactly once", () => { const store = newStore(); const s = store.create(0, "10.0.0.1", "ua"); assert.equal(store.decide(s, "michal", "10.0.0.2", s.number), "approved"); assert.equal(store.consume(s.id, 1000), "michal"); // Single-use: a replay of the same id must assert nobody. This is what stops // a captured session id from being redeemed twice. assert.equal(store.consume(s.id, 1000), undefined); assert.equal(store.get(s.id, 1000)?.status, "consumed"); }); test("a wrong number denies outright and never allows a retry", () => { const store = newStore(); const s = store.create(0, "10.0.0.1", "ua"); const wrong = s.number === 99 ? 98 : s.number + 1; assert.equal(store.decide(s, "michal", "10.0.0.2", wrong), "denied"); // The correct answer afterwards must NOT rescue it: an unbounded retry loop // would turn 6.5 bits of entropy back into none. assert.equal(store.decide(s, "michal", "10.0.0.2", s.number), "denied"); assert.equal(store.consume(s.id, 1000), undefined); }); test("an explicit deny (null) is honoured", () => { const store = newStore(); const s = store.create(0, "10.0.0.1", "ua"); assert.equal(store.decide(s, "michal", "10.0.0.2", null), "denied"); assert.equal(store.consume(s.id, 1000), undefined); }); test("only approved sessions can be consumed", () => { const store = newStore(); const s = store.create(0, "10.0.0.1", "ua"); assert.equal(store.consume(s.id, 1000), undefined, "pending must not assert a user"); }); test("sessions expire, and expiry frees the approval token", () => { const store = newStore({ ttlSeconds: 1 }); const s = store.create(0, "10.0.0.1", "ua"); assert.ok(store.get(s.id, 500)); assert.equal(store.get(s.id, 1_001), undefined); assert.equal(store.byToken(s.approvalToken, 1_001), undefined); assert.equal(store.size, 0); }); test("the store is bounded, and sweeping reclaims room", () => { const store = newStore({ maxSessions: 2, ttlSeconds: 1 }); store.create(0, "10.0.0.1", "ua"); store.create(0, "10.0.0.1", "ua"); assert.throws(() => store.create(0, "10.0.0.1", "ua"), /too many/); // Once the first two lapse the sweep on create() reclaims them, so a flood // is bounded memory rather than a permanent outage. assert.ok(store.create(2_000, "10.0.0.1", "ua")); }); test("numbers are two digits and decoys are distinct from the answer", () => { for (let i = 0; i < 200; i++) { const n = pickNumber(); assert.ok(n >= 10 && n <= 99, `${n} out of range`); } const answer = pickNumber(); const choices = pickDecoys(answer); assert.equal(choices.length, 3); assert.equal(new Set(choices).size, 3, "decoys must not repeat"); assert.ok(choices.includes(answer), "the answer must be among the choices"); }); test("secretEquals rejects different lengths without throwing", () => { assert.equal(secretEquals("abc", "abcd"), false); assert.equal(secretEquals("abc", "abc"), true); assert.equal(secretEquals("abc", "abd"), false); });