Files

145 lines
5.7 KiB
JavaScript
Raw Permalink Normal View History

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');
});