82 lines
5.7 KiB
JavaScript
82 lines
5.7 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
const secret = 'sk-test-NOT-A-SECRET-1234';
|
|
function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r4-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; }
|
|
function safe(label, Type, props = {}, omitted = secret) { return error => {
|
|
assert.ok(error instanceof Type, error.stack);
|
|
for (const [key, value] of Object.entries(props)) assert.equal(error[key], value);
|
|
assert.ok(error.message.includes(label), error.message);
|
|
assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET|Unexpected token/);
|
|
assert.ok(!error.message.includes(omitted), error.message);
|
|
assert.equal(error.cause, undefined);
|
|
return true;
|
|
}; }
|
|
import { checkAccess } from '../src/access.mjs';
|
|
import { checkAppearance } from '../src/appearance.mjs';
|
|
import { loadProject } from '../src/project.mjs';
|
|
import { validateRoleDocument } from '../src/role.mjs';
|
|
import { covers } from '../src/vocabulary.mjs';
|
|
import { BusinessError } from '../src/errors.mjs';
|
|
const context = { providers: ['test'], peers: [], businesses: { acme: { human: 'jason', instances: ['coder'] } }, ssoSubjects: [] };
|
|
const access = () => ({ accessVersion: 1, revision: 1, users: [{ id: 'bean', home: 'local', subjects: [{ provider: 'test', sub: 'user' }] }], grants: [{ id: 'grant', user: 'bean', business: 'acme', projects: ['*'], instances: ['coder'], level: 'viewer' }] });
|
|
for (const [field, edit] of [
|
|
['users[0].home', d => { d.users[0].home = secret; }],
|
|
['users[0].subjects[0].provider', d => { d.users[0].subjects[0].provider = secret; }],
|
|
...['user', 'business', 'level'].map(k => [`grants[0].${k}`, d => { d.grants[0][k] = secret; }]),
|
|
['grants[0].projects[0]', d => { d.grants[0].projects = [secret]; }],
|
|
['accessVersion', d => { d.accessVersion = secret; }],
|
|
]) test(`access ${field} omits the refused value`, () => {
|
|
const d = access(); edit(d); assert.throws(() => checkAccess(d, context), safe(field, BusinessError, { exitCode: 2 }));
|
|
});
|
|
for (const k of ['theme', 'mode']) test(`appearance ${k} omits its value`, () => {
|
|
assert.throws(() => checkAppearance({ [k]: secret }, 'appearance'), safe(k, BusinessError, { exitCode: 2 }));
|
|
});
|
|
test('project root omits its configured value', () => assert.throws(() => loadProject(secret), safe('project root', BusinessError, { exitCode: 2 })));
|
|
test('covers names the level field, not its value', () => assert.throws(() => covers({}, {}, new Date(), { level: secret }), safe('level', TypeError)));
|
|
const role = () => ({ roleVersion: 2, name: 'r', title: 'R', contract: 'r.md', tools: ['read'], network: 'none', authority: { withinRole: [], crossRole: [] }, credentials: [] });
|
|
for (const [field, edit] of [
|
|
['skill', d => { d.skills = [secret + '/']; }],
|
|
['contract', d => { d.contract = secret; }],
|
|
['tool', d => { d.tools = [secret]; }],
|
|
['authority.withinRole', d => { d.authority.withinRole = [secret]; }],
|
|
['hireable', d => { d.hireable = [secret.toLowerCase()]; }],
|
|
['gitea scope', d => { d.credentials = [{ service: 'gitea', scopes: [secret] }]; }],
|
|
['vikunja route group', d => { d.credentials = [{ service: 'vikunja', scopes: { [secret]: ['read'] } }]; }],
|
|
['vikunja verb', d => { d.credentials = [{ service: 'vikunja', scopes: { tasks: [secret] } }]; }],
|
|
['name', d => { d.name = secret.toLowerCase(); }],
|
|
]) test(`role ${field} omits the configured value`, t => {
|
|
const root = scratch(t); writeFileSync(join(root, 'r.md'), '# R\n\n## Authority\n');
|
|
const d = role(); edit(d);
|
|
assert.throws(() => validateRoleDocument(d, join(root, 'r.json')), safe(field, BusinessError, { exitCode: 2 }, secret.toLowerCase()));
|
|
});
|
|
// This branch is reachable only with already-validated service names; a sentinel service
|
|
// is refused earlier. A known service is already displayed in the role's rails.
|
|
test('creative service refusal names the validated service list', t => {
|
|
const root = scratch(t); writeFileSync(join(root, 'companion.md'), '# R\n\n## Authority\n');
|
|
const d = { ...role(), name: 'companion', contract: 'companion.md', messaging: { sendTo: ['human'], receiveFrom: ['human'] }, credentials: [{ service: 'vikunja', scopes: { tasks: ['read_one'] } }] };
|
|
assert.throws(() => validateRoleDocument(d, join(root, 'companion.json')), named('credential', 'vikunja'));
|
|
});
|
|
test('role duplicate skill omits the accepted list value', t => {
|
|
const root = scratch(t); writeFileSync(join(root, 'r.md'), '# R\n\n## Authority\n');
|
|
const d = { ...role(), skills: [secret, secret] };
|
|
assert.throws(() => validateRoleDocument(d, join(root, 'r.json')), safe('role skills', BusinessError, { exitCode: 2 }));
|
|
});
|
|
for (const at of ['user', 'grant', 'scope', 'human']) test(`access ${at} duplicate/reserved refusal names accepted ids`, () => {
|
|
const value = secret.toLowerCase(), d = access(), ctx = structuredClone(context);
|
|
if (at === 'user') { d.users[0].id = value; d.users.push({ ...structuredClone(d.users[0]), subjects: [{ provider: 'test', sub: 'second' }] }); }
|
|
if (at === 'grant') { d.grants[0].id = value; d.grants.push(structuredClone(d.grants[0])); }
|
|
if (at === 'scope') { ctx.businesses.acme.instances = [value]; d.grants[0].instances = [value, value]; }
|
|
if (at === 'human') { ctx.businesses.acme.human = value; d.users[0].id = value; }
|
|
assert.throws(() => checkAccess(d, ctx), named(at === 'scope' ? 'instances' : '.id', value));
|
|
});
|
|
|
|
function named(label, value) { return error => {
|
|
assert.ok(error instanceof BusinessError); assert.equal(error.exitCode, 2);
|
|
assert.ok(error.message.includes(label), error.message);
|
|
assert.ok(error.message.includes(value), error.message);
|
|
return true;
|
|
}; }
|