Files
loyaly-merchant/scripts/staff-access.test.mts
2026-09-25 16:29:41 +05:30

96 lines
3.6 KiB
TypeScript

/**
* The staff access policy the proxy enforces (src/features/auth/services/
* staffAccess.ts). Run with `npm run test:access` — Node's built-in runner, no
* dependencies. Covers the role matrix only; tenant isolation is the
* platform's (the company comes from the upstream token, never the request).
*/
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {
STAFF_HOME,
isStaffPage,
isStaffRole,
staffApiDecision,
} from '../src/features/auth/services/staffAccess.ts';
const q = (o: Record<string, string> = {}) => new URLSearchParams(o);
const one = q({storeId: 'chennai', range: '30d'});
test('only role=staff is gated', () => {
assert.equal(isStaffRole('staff'), true);
for (const r of ['owner', 'manager', 'admin', undefined, '', 'STAFF']) {
assert.equal(isStaffRole(r), false, String(r));
}
});
test('staff home is a staff page', () => {
assert.equal(isStaffPage(STAFF_HOME), true);
});
test('staff pages: floor work allowed, merchant pages refused', () => {
for (const p of ['/floor', '/customers', '/customers/abc', '/activity', '/settings/profile', '/settings/security']) {
assert.equal(isStaffPage(p), true, p);
}
for (const p of ['/dashboard', '/commerce', '/stores', '/lyts', '/staff', '/settings', '/settings/team', '/settings/billing', '/settings/roles', '/settings/stores', '/admin', '/floorplan', '/customersX']) {
assert.equal(isStaffPage(p), false, p);
}
});
test('staff APIs: floor work allowed with one store', () => {
const allow: [string, string, URLSearchParams][] = [
['GET', '/api/sites', q()],
['GET', '/api/floor/visits', one],
['POST', '/api/visits/v1/attend', q()],
['POST', '/api/visits/v1/release', q()],
['POST', '/api/visits/v1/complete', q()],
['GET', '/api/visits', one],
['GET', '/api/visits/stream', q({storeId: 'chennai'})],
['POST', '/api/customers', q()],
['GET', '/api/visitors', q()],
['GET', '/api/visitors/x/history', q()],
['GET', '/api/visitors/x/image', q()],
['PUT', '/api/visitors/x/profile', q()],
['GET', '/api/faces', q({src: '/api/faces/a'})],
['POST', '/api/sales', q()],
['POST', '/api/purchases', q()],
['GET', '/api/health', q()],
];
for (const [m, p, s] of allow) assert.equal(staffApiDecision(m, p, s), 'allow', `${m} ${p}`);
});
test('staff APIs: "All stores" or no store is refused on scoped reads', () => {
for (const p of ['/api/floor/visits', '/api/visits', '/api/visits/stream']) {
assert.equal(staffApiDecision('GET', p, q({storeId: 'all'})), 'needs_store', p);
assert.equal(staffApiDecision('GET', p, q()), 'needs_store', p);
}
});
test('staff APIs: merchant-only surfaces are forbidden', () => {
const deny: [string, string][] = [
['GET', '/api/reports/footfall'],
['GET', '/api/reports/conversion'],
['GET', '/api/reports/journey'],
['GET', '/api/dashboard/summary'],
['GET', '/api/sales'],
['GET', '/api/sales/abc'],
['GET', '/api/team'],
['GET', '/api/team/invitations'],
['GET', '/api/cameras'],
['GET', '/api/cameras/c1/live'],
['GET', '/api/images'],
['GET', '/api/campaigns'],
['GET', '/api/activities'],
['POST', '/api/assistant'],
['POST', '/api/sites'],
['PATCH', '/api/sites/chennai'],
['DELETE', '/api/sites/chennai'],
['DELETE', '/api/visitors/x'],
['GET', '/api/admin/clients'],
['GET', '/api/unknown-new-route'],
// Method matters: an allowed path with the wrong verb is refused.
['DELETE', '/api/sales'],
['POST', '/api/floor/visits'],
];
for (const [m, p] of deny) assert.equal(staffApiDecision(m, p, one), 'forbidden', `${m} ${p}`);
});