import test from 'node:test'; import assert from 'node:assert/strict'; import { permittedDomains, checkDomain, stripPhonePrefix, CACHE_FRESH, } from '../permit.js'; import { fakeFetch } from './helpers.js'; const CATALOG = [ { code: 'uc', name: 'UNICAL' }, { code: 'mtr', name: 'Metropolitan Hotel' }, { code: 'ucth', name: 'UCTH' }, ]; const PORTAL = 'https://portal.example'; test('stripPhonePrefix removes a single leading +', () => { assert.equal(stripPhonePrefix('+2348068287222'), '2348068287222'); assert.equal(stripPhonePrefix('2348068287222'), '2348068287222'); assert.equal(stripPhonePrefix(undefined), ''); }); test('checkDomain verifies payload shape and allows on first object', async () => { const fetchImpl = fakeFetch(({ body }) => ({ body: { ok: true, allowed: true } })); const allowed = await checkDomain(fetchImpl, PORTAL, '2348068287222', 'uc'); assert.equal(allowed, true); assert.equal(fetchImpl.calls.length, 1); assert.equal(fetchImpl.calls[0].url, `${PORTAL}/api/openfga/verify`); assert.deepEqual(fetchImpl.calls[0].body, { subsystem: 'idid', user: 'user:2348068287222', relation: 'capture_biometric', object: 'domain:uc', }); }); test('checkDomain falls back to domain:idid and allows', async () => { const fetchImpl = fakeFetch(({ body }) => ({ body: { ok: true, allowed: body.object === 'domain:idid' }, })); const allowed = await checkDomain(fetchImpl, PORTAL, '2348068287222', 'uc'); assert.equal(allowed, true); assert.deepEqual( fetchImpl.calls.map((c) => c.body.object), ['domain:uc', 'domain:idid'] ); }); test('checkDomain denies after both objects deny', async () => { const fetchImpl = fakeFetch(() => ({ body: { ok: true, allowed: false } })); const allowed = await checkDomain(fetchImpl, PORTAL, '2348068287222', 'uc'); assert.equal(allowed, false); assert.equal(fetchImpl.calls.length, 2); }); test('checkDomain throws (fail closed) when neither object answers', async () => { const fetchImpl = fakeFetch(() => ({ status: 503, body: {} })); await assert.rejects(() => checkDomain(fetchImpl, PORTAL, '2348068287222', 'uc')); }); test('permittedDomains keeps only allowed domains and fails closed per domain', async () => { const fetchImpl = fakeFetch(({ body }) => { if (body.object === 'domain:uc') return { body: { ok: true, allowed: true } }; if (body.object === 'domain:mtr') return { body: { ok: true, allowed: false } }; return { status: 500, body: {} }; // ucth: errors on both objects → excluded }); const cache = new Map(); const { codes, checkedAt } = await permittedDomains({ domains: CATALOG, phone: '234', portalUrl: PORTAL, fetchImpl, cache, now: () => 1000, }); assert.deepEqual(codes, ['uc']); assert.equal(checkedAt, 1000); }); test('permittedDomains serves the cache within the TTL without portal calls', async () => { const fetchImpl = fakeFetch(() => ({ body: { ok: true, allowed: true } })); const cache = new Map(); let t = 1000; const now = () => t; const opts = { domains: CATALOG, phone: '234', portalUrl: PORTAL, fetchImpl, cache, now }; const first = await permittedDomains(opts); const callsAfterFirst = fetchImpl.calls.length; t += 60 * 1000; // 1 minute later — still fresh const second = await permittedDomains(opts); assert.equal(fetchImpl.calls.length, callsAfterFirst, 'no portal calls while cached'); assert.deepEqual(second, first); }); test('permittedDomains re-checks after the TTL expires', async () => { const fetchImpl = fakeFetch(() => ({ body: { ok: true, allowed: true } })); const cache = new Map(); let t = 1000; const now = () => t; const opts = { domains: CATALOG, phone: '234', portalUrl: PORTAL, fetchImpl, cache, now }; await permittedDomains(opts); const callsAfterFirst = fetchImpl.calls.length; t += 5 * 60 * 1000 + 1; const { checkedAt } = await permittedDomains(opts); assert.ok(fetchImpl.calls.length > callsAfterFirst, 'portal re-consulted after expiry'); assert.equal(checkedAt, t); }); test('forced refresh refuses while the cache is fresh', async () => { const fetchImpl = fakeFetch(() => ({ body: { ok: true, allowed: true } })); const cache = new Map(); let t = 1000; const opts = { domains: CATALOG, phone: '234', portalUrl: PORTAL, fetchImpl, cache, now: () => t }; await permittedDomains(opts); const callsAfterFirst = fetchImpl.calls.length; t += 1000; await assert.rejects( () => permittedDomains({ ...opts, force: true }), (e) => e.code === CACHE_FRESH ); assert.equal(fetchImpl.calls.length, callsAfterFirst, 'refused refresh makes no portal calls'); }); test('forced refresh after expiry re-checks and replaces the cache', async () => { const fetchImpl = fakeFetch(({ body }) => ({ body: { ok: true, allowed: body.object === 'domain:uc' }, })); const cache = new Map(); let t = 1000; const opts = { domains: CATALOG, phone: '234', portalUrl: PORTAL, fetchImpl, cache, now: () => t }; await permittedDomains(opts); // everything allowed t += 5 * 60 * 1000 + 1; const forced = await permittedDomains({ ...opts, force: true }); assert.deepEqual(forced.codes, ['uc']); // cache was replaced: a plain read now serves the forced result const plain = await permittedDomains(opts); assert.deepEqual(plain, forced); }); test('cache is per phone', async () => { const fetchImpl = fakeFetch(() => ({ body: { ok: true, allowed: true } })); const cache = new Map(); const opts = { domains: CATALOG, portalUrl: PORTAL, fetchImpl, cache, now: () => 1000 }; await permittedDomains({ ...opts, phone: 'a' }); const callsAfterA = fetchImpl.calls.length; await permittedDomains({ ...opts, phone: 'b' }); assert.ok(fetchImpl.calls.length > callsAfterA, 'second phone performs its own checks'); });