Files
stack/packages/webui/tests/org-list-privacy.test.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

63 lines
3.5 KiB
JavaScript

// The organization list must not reveal an org to a principal who does not own it, through
// either an invalid row or the 503 message (polish review F1, F2; P19 #868).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createOrg, showOrg, templateRender } from '../../organization/src/org-service.mjs';
import { listOrgs, resolveOrg } from '../../organization/src/org-registry.mjs';
import { organizationConsole } from '../src/organizations.mjs';
import { startServer } from '../src/serve.mjs';
import { createDevOwner } from '../src/dev-owner.mjs';
const principal = { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' };
async function context(t) {
const root = await fs.mkdtemp(join(tmpdir(), 'orgpriv-'));
const ctx = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal };
await fs.mkdir(ctx.configDir, { mode: 0o700 }); await fs.mkdir(ctx.dataRoot, { mode: 0o700 });
t.after(() => fs.rm(root, { recursive: true, force: true }));
return ctx;
}
const adapter = ctx => organizationConsole({ ctx, listOrgs, showOrg, resolveOrg, orgPaths: () => ({}), busStatus: async () => ({ healthy: true }), options: () => ({ reads: {}, bus: {} }) });
async function serverFor(t, organizations, session = principal) {
const sso = { ...createDevOwner(), session: () => ({ ...session, name: 'Test operator' }) };
const s = await startServer({ port: 0, sso, organizations, log: () => {} });
t.after(() => new Promise(r => { s.close(r); s.closeIdleConnections(); }));
return `http://127.0.0.1:${s.address().port}`;
}
test('a foreign org with a broken root stays hidden', async t => {
const ctx = await context(t);
await createOrg(ctx, 'private-tenant', await templateRender('private-tenant'));
const orgRoot = join(ctx.dataRoot, 'org-runtime', 'private-tenant');
await fs.writeFile(join(orgRoot, 'owner.json'), JSON.stringify({ ...principal, sub: 'different-owner' }));
const orgs = adapter(ctx);
assert.deepEqual(await orgs.list(principal), []);
await fs.unlink(join(orgRoot, '.mosaic-org.json'));
const response = await fetch(`${await serverFor(t, orgs)}/api/orgs`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), []);
});
test('the owner of a broken org sees it as invalid, with paths masked', async t => {
const ctx = await context(t);
await createOrg(ctx, 'alpha', await templateRender('alpha'));
await fs.unlink(join(ctx.configDir, 'businesses', 'alpha.json'));
const rows = await adapter(ctx).list(principal);
assert.equal(rows.length, 1); assert.equal(rows[0].id, 'alpha'); assert.equal(rows[0].invalid, true);
assert.equal(rows[0].reason.includes(ctx.configDir), false);
});
test('a malformed registry gives every caller a fixed 503 message with no file bytes', async t => {
const ctx = await context(t);
await createOrg(ctx, 'private-tenant', await templateRender('private-tenant'));
const outsider = { ...principal, sub: 'outsider' };
const base = await serverFor(t, adapter(ctx), outsider);
assert.deepEqual(await (await fetch(`${base}/api/orgs`)).json(), []);
await fs.writeFile(join(ctx.configDir, 'org-runtime.json'), 'private-tenant');
const response = await fetch(`${base}/api/orgs`), body = await response.json();
assert.equal(response.status, 503);
assert.deepEqual(body, { error: 'org-unavailable', message: 'Organization registry could not be read' });
});