Files
stack/packages/organization/tests/safe-names.test.mjs

78 lines
4.9 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { validateRoleDocument } from '../src/role.mjs';
import { checkAccess } from '../src/access.mjs';
import { BusinessError } from '../src/errors.mjs';
const bad = 'sk-test-NOT-A-SECRET-1234';
const badId = `${bad.toLowerCase()} "`;
function refusal(fn, names = [], omitted = bad, validation = null) {
assert.throws(fn, error => {
assert.ok(error instanceof BusinessError);
assert.equal(error.exitCode, 2);
for (const name of names) assert.ok(error.message.includes(name), error.message);
assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET/i);
assert.ok(!error.message.includes(omitted), error.message);
if (validation !== null) assert.match(error.message, validation);
return true;
});
}
function role(t, edit = () => {}) {
const root = mkdtempSync(join(tmpdir(), 'names-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
writeFileSync(join(root, 'r.md'), '# R\n\n## Authority\n');
writeFileSync(join(root, 'companion.md'), '# R\n\n## Authority\n');
const doc = { roleVersion: 2, name: 'r', title: 'R', contract: 'r.md', tools: ['read'], network: 'none',
authority: { withinRole: [], crossRole: [] }, credentials: [] };
edit(doc, root);
return () => validateRoleDocument(doc, join(root, `${doc.name}.json`));
}
for (const key of ['withinRole', 'crossRole']) test(`gated ${key} names the known action; unknown action stays unnamed`, t => {
refusal(role(t, d => { d.authority[key] = ['deploy']; }), ['authority.' + key, 'deploy']);
refusal(role(t, d => { d.authority[key] = [bad]; }));
});
test('overlapping authority lists name every known action; invalid entries stop first', t => {
refusal(role(t, d => { d.authority = { withinRole: ['message.send', 'record.write'], crossRole: ['message.send', 'record.write'] }; }), ['message.send', 'record.write']);
refusal(role(t, d => { d.authority = { withinRole: [bad], crossRole: [bad] }; }));
});
test('removed hireable key refuses without echoing its value', t => {
refusal(role(t, d => { d.hireable = [badId]; }), ['hireable'], badId, /unsupported role key/);
});
test('gitea category conflict names the checked category; invalid scope stops first', t => {
refusal(role(t, d => { d.credentials = [{ service: 'gitea', scopes: ['read:issue', 'write:issue'] }]; }), ['issue']);
refusal(role(t, d => { d.credentials = [{ service: 'gitea', scopes: [bad] }]; }));
});
test('duplicate credential names the checked service; invalid service stops first', t => {
refusal(role(t, d => { d.credentials = [{ service: 'gitea', scopes: ['read:issue'] }, { service: 'gitea', scopes: ['read:issue'] }]; }), ['gitea']);
refusal(role(t, d => { d.credentials = [{ service: bad }, { service: bad }]; }));
});
for (const field of ['tools', 'credentials']) test(`creative ${field} names the checked wider grant; invalid grant stops first`, t => {
const edit = d => { d.name = 'companion'; d.contract = 'companion.md'; d.messaging = { sendTo: ['human'], receiveFrom: ['human'] }; };
refusal(role(t, d => { edit(d); if (field === 'tools') d.tools.push('bash'); else d.credentials = [{ service: 'vikunja', scopes: { tasks: ['read_one'] } }]; }), [field === 'tools' ? 'bash' : 'vikunja']);
refusal(role(t, d => { edit(d); if (field === 'tools') d.tools = [bad]; else d.credentials = [{ service: bad }]; }));
});
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: 'one' }] }],
grants: [{ id: 'grant', user: 'bean', business: 'acme', projects: ['*'], instances: ['coder'], level: 'viewer' }] });
for (const kind of ['reserved', 'human', 'user', 'grant', 'project', 'instance']) test(`access ${kind} conflict names checked id; malformed replacement stops first`, () => {
function run(value) {
const d = access(), ctx = context();
if (kind === 'reserved' || kind === 'human') d.users[0].id = value;
if (kind === 'user') d.users.push({ ...structuredClone(d.users[0]), id: value, subjects: [{ provider: 'test', sub: 'two' }] });
if (kind === 'grant') { d.grants[0].id = value; d.grants.push(structuredClone(d.grants[0])); }
if (kind === 'project') d.grants[0].projects = [value, value];
if (kind === 'instance') d.grants[0].instances = [value, value];
return () => checkAccess(d, ctx);
}
const value = { reserved: 'human', human: 'jason', user: 'bean', grant: 'g-known-1', project: '11111111-2222-4333-8444-555555555555', instance: 'coder' }[kind];
refusal(run(value), [value]);
const validation = kind === 'project' ? /lowercase UUID/ : kind === 'instance' ? /declared instance/ : /\.id must match/;
refusal(run(badId), [], badId, validation);
});