Files
baron_qa_write/tests/org-verification.test.js
T
root e2a6168744
Deploy EG-BIM QA Gateway / deploy (push) Failing after 54s
fix(ci): classify organization API access denials safely
Report documented operation, scope and exposure error codes, permitted exposure enums and proxy challenge indicators during deployment checks. Exclude arbitrary upstream messages and credential material while retaining failure on denied access.

Validation: source checks and 123 tests pass. Retried staging run 227194 still returns org_context_http_403; the new diagnostic requires the user-managed push before a further run.
2026-11-11 12:36:00 +09:00

165 lines
7.6 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]);
});
for (const code of ['credential_operation_mismatch', 'credential_scope_exceeded', 'credential_exposure_exceeded']) {
test(`access diagnostics preserve the documented ${code} reason while excluding upstream messages`, async () => {
const logs = [];
await assert.rejects(verifyOrganization(settings(), {
fetchImpl: async () => response({ code, message: 'private-server-message', details: {
grantedExposure: 'members', requiredExposure: 'identifiers', secret: 'private-value'
} }, 403),
log: line => logs.push(line)
}), /org_context_http_403/);
const diagnostic = JSON.parse(logs[0]);
assert.equal(diagnostic.upstreamCode, code);
assert.equal(diagnostic.grantedExposure, 'members');
assert.equal(diagnostic.requiredExposure, 'identifiers');
assert.ok(!logs[0].includes('private-server-message'));
assert.ok(!logs[0].includes('private-value'));
});
}
test('unrecognized codes and invalid enum values cannot expose private upstream strings', async () => {
const logs = [];
await assert.rejects(verifyOrganization(settings(), {
fetchImpl: async () => response({ code: 'private-code', details: {
grantedExposure: 'private-granted', requiredExposure: 'private-required'
} }, 403),
log: line => logs.push(line)
}), /org_context_http_403/);
const diagnostic = JSON.parse(logs[0]);
assert.equal(diagnostic.upstreamCode, 'unrecognized');
assert.equal(diagnostic.grantedExposure, undefined);
assert.equal(diagnostic.requiredExposure, undefined);
assert.ok(!logs[0].includes('private-'));
});
test('HTML proxy challenges are distinguishable without logging HTML or bypassing the failure', async () => {
const logs = [];
await assert.rejects(verifyOrganization(settings(), {
fetchImpl: async () => new Response('<html>private-challenge-body</html>', {
status: 403, headers: { 'content-type': 'text/html', 'cf-mitigated': 'challenge' }
}),
log: line => logs.push(line)
}), /org_context_http_403/);
const diagnostic = JSON.parse(logs[0]);
assert.equal(diagnostic.responseKind, 'html');
assert.equal(diagnostic.proxyChallenge, true);
assert.equal(diagnostic.upstreamCode, 'unrecognized');
assert.ok(!logs[0].includes('private-challenge-body'));
});