Files
baron_qa_write/tests/org-verification.test.js
T
root bed970a175 fix(ci): identify denied organization roots in access checks
Log the requested tenant, endpoint, operation and exposure for organization HTTP 401/403 responses. Keep credentials and upstream response bodies out of diagnostics and retain the deployment gate.

Validation: source checks and 118 tests pass, including forbidden-root identification and diagnostic privacy coverage.
2026-11-11 11:35:36 +09:00

117 lines
5.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { verifyOrganization } from '../scripts/verify-org-context.mjs';
const ROOT = '038326b6-954a-48a7-a85f-efd83f62b82a';
const USER = '01900000-0000-7000-8000-000000000001';
let sequence = 0;
const settings = () => ({
ORG_CONTEXT_ENABLED: 'true', ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT]),
ORG_CONTEXT_KEY_ID: 'fixture-verification', ORG_CONTEXT_KEY_SECRET: 'fixture-secret-' + ++sequence
});
const directory = () => ({ scope: { tenantId: ROOT }, tree: { id: ROOT, name: '기준 조직', members: [{ id: USER }] } });
const response = (body, status = 200) => Response.json(body, { status });
for (const status of [429, 502, 503, 504]) {
test(`deployment verification recovers from HTTP ${status} and reports only verified directory counts`, async () => {
let calls = 0;
const logs = [], pauses = [];
const result = await verifyOrganization(settings(), {
fetchImpl: async () => ++calls === 1 ? response({}, status) : response(directory()),
pause: async (ms) => pauses.push(ms), log: (line) => logs.push(line)
});
assert.equal(calls, 2);
assert.deepEqual(pauses, [2000]);
assert.equal(result.status, 'verified');
assert.equal(result.userCount, 1);
assert.ok(!JSON.stringify(logs).includes('fixture-secret'));
assert.ok(!JSON.stringify(logs).includes(USER));
});
}
test('deployment verification allows 15 seconds per request and retries an actual abort', async (t) => {
const originalTimer = globalThis.setTimeout;
const timeouts = [];
t.mock.method(globalThis, 'setTimeout', (callback, delay) => {
timeouts.push(delay);
return originalTimer(callback, delay === 15000 ? 5 : delay);
});
let calls = 0;
const result = await verifyOrganization(settings(), {
fetchImpl: async (_url, { signal }) => {
if (++calls === 1) return new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
});
return response(directory());
},
pause: async () => {}, log() {}
});
assert.equal(calls, 2);
assert.deepEqual(timeouts, [15000, 15000]);
assert.equal(result.status, 'verified');
});
test('persistent network failure still blocks deployment after three attempts without exposing upstream errors', async () => {
let calls = 0, pauses = 0;
const logs = [];
await assert.rejects(verifyOrganization(settings(), {
fetchImpl: async () => { calls++; throw new Error('private-network-detail'); },
pause: async () => { pauses++; }, log: (line) => logs.push(line)
}), /org_context_request_failed/);
assert.equal(calls, 3);
assert.equal(pauses, 2);
assert.ok(!JSON.stringify(logs).includes('private-network-detail'));
});
for (const [name, body, status, error] of [
['unauthorized', {}, 401, 'http_401'],
['forbidden', {}, 403, 'http_403'],
['wrong scope', { scope: { tenantId: USER } }, 200, 'scope_mismatch'],
['missing members', { scope: { tenantId: ROOT }, tree: { id: ROOT, name: 'Empty' } }, 200, 'members_missing']
]) {
test(`deployment verification rejects ${name} immediately`, async () => {
let calls = 0;
await assert.rejects(verifyOrganization(settings(), {
fetchImpl: async () => { calls++; return response(body, status); },
pause: async () => assert.fail('must not retry a configuration or data error'), log() {}
}), new RegExp('org_context_' + error));
assert.equal(calls, 1);
});
}
test('disabled verification makes no upstream requests', async () => {
await verifyOrganization({ ORG_CONTEXT_ENABLED: 'false' }, {
fetchImpl: async () => assert.fail('disabled lookup'), log() {}
});
});
for (const status of [401, 403]) {
test(`HTTP ${status} diagnostics identify the requested tenant without exposing credentials or upstream data`, async () => {
const env = settings();
const logs = [];
await assert.rejects(verifyOrganization(env, {
fetchImpl: async () => response({ error: 'private-upstream-message', name: 'private-user', phone: '+8201000000000' }, status),
log: line => logs.push(line), pause: async () => assert.fail('must not retry access denial')
}), new RegExp('org_context_http_' + status));
const diagnostic = JSON.parse(logs[0]);
assert.equal(diagnostic.httpStatus, status);
assert.equal(diagnostic.endpoint, 'https://sadmin.hmac.kr/api/v1/integrations/org-context');
assert.equal(diagnostic.tenantId, ROOT);
assert.equal(diagnostic.operation, 'baron.org-context.read');
assert.equal(diagnostic.exposure, 'identifiers');
for (const hidden of [env.ORG_CONTEXT_KEY_ID, env.ORG_CONTEXT_KEY_SECRET, 'private-upstream-message', 'private-user', '+8201000000000']) {
assert.ok(!JSON.stringify(logs).includes(hidden));
}
});
}
test('a partial directory fails and diagnostics point to the forbidden root instead of a permitted root', async () => {
const deniedRoot = '9951229e-eea5-4376-96f4-78989bc3a506';
const logs = [];
await assert.rejects(verifyOrganization({ ...settings(), ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT, deniedRoot]) }, {
fetchImpl: async url => new URL(url).searchParams.get('tenantId') === deniedRoot ? response({}, 403) : response(directory()),
log: line => logs.push(line), pause: async () => assert.fail('must not retry access denial')
}), /org_context_http_403/);
assert.deepEqual(logs.map(line => JSON.parse(line).tenantId), [deniedRoot]);
});