Files

211 lines
6.2 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
APPROVAL_REQUEST_SCHEMA,
APPROVED_ACTION_DISPATCH_SCHEMA,
ApprovalHumanDecisionRequiredError,
ApprovalMutationConflictError,
ApprovalRequestExpiredError,
ApprovalRequestVersionConflictError,
ApprovalSeparationOfDutyError,
InvalidApprovedActionValueError,
approvalRequestEffectiveStatus,
consumeApprovalRequest,
createApprovalRequest,
decideApprovalRequest,
normalizeApprovalRequestRecord,
normalizeApprovedActionDispatchRecord,
} = require('@qinglong/runtime-core/approved-action');
const DIGEST_A = 'a'.repeat(64);
const DIGEST_B = 'b'.repeat(64);
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
const REVIEWER = Object.freeze({ type: 'user', id: 'usr_reviewer' });
const SYSTEM = Object.freeze({ type: 'system', id: 'approved-dispatcher' });
const FENCE = Object.freeze({ projectVersion: 2, bindingVersion: 3 });
function action(overrides = {}) {
return {
permission: 'package.manage',
actionType: 'plugin_package.install',
actionRef: 'proposal:pkg-demo-v1',
actionDigest: DIGEST_A,
previewDigest: DIGEST_B,
...overrides,
};
}
function request(overrides = {}) {
return createApprovalRequest({
id: 'approval-1',
projectId: 'default',
action: action(),
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: REQUESTER,
requestedAtMs: 1_000,
expiresAtMs: 61_000,
requestFence: FENCE,
...overrides,
});
}
function principal(subject = REQUESTER, overrides = {}) {
return {
subject,
authenticationId: 'auth-step-up-1',
authenticatedAtMs: 1_500,
expiresAtMs: 10_000,
assurance: 'local_console',
...overrides,
};
}
function approve(current, overrides = {}) {
return decideApprovalRequest(current, {
expectedVersion: 1,
decisionId: 'decision-1',
decision: 'approved',
reasonCode: 'reviewed',
principal: principal(),
decidedAtMs: 2_000,
authorizationFence: FENCE,
...overrides,
});
}
function consume(current, overrides = {}) {
return consumeApprovalRequest(current, {
expectedVersion: 2,
consumptionId: 'consume-1',
dispatchId: 'dispatch-1',
action: action(),
requestedBy: REQUESTER,
consumedBy: SYSTEM,
consumedAtMs: 3_000,
authorizationFence: FENCE,
...overrides,
});
}
test('builds a digest-bound request and reports pending expiry without mutation', () => {
const created = request();
assert.equal(created.schema, APPROVAL_REQUEST_SCHEMA);
assert.equal(created.version, 1);
assert.equal(created.state, 'pending');
assert.equal(created.action.permission, 'package.manage');
assert.equal(approvalRequestEffectiveStatus(created, 60_999), 'pending');
assert.equal(approvalRequestEffectiveStatus(created, 61_000), 'expired');
assert.equal(created.state, 'pending');
assert.throws(
() => request({ expiresAtMs: 1_000 + 24 * 60 * 60 * 1_000 + 1 }),
InvalidApprovedActionValueError,
);
});
test('allows strong same-user confirmation for owner-only edge deployments', () => {
const decided = approve(request());
assert.equal(decided.version, 2);
assert.equal(decided.state, 'approved');
assert.deepEqual(decided.decidedBy, REQUESTER);
assert.equal(decided.decisionAssurance, 'local_console');
const result = consume(decided);
assert.equal(result.request.version, 3);
assert.equal(result.request.state, 'consumed');
assert.equal(result.dispatch.schema, APPROVED_ACTION_DISPATCH_SCHEMA);
assert.equal(result.dispatch.approvalRequestVersion, 3);
assert.equal(result.dispatch.action.actionDigest, DIGEST_A);
assert.equal(result.dispatch.approvedBy.id, REQUESTER.id);
assert.deepEqual(result.dispatch.approvalFence, FENCE);
assert.deepEqual(
normalizeApprovedActionDispatchRecord(result.dispatch),
result.dispatch,
);
});
test('enforces separation of duty when the project ceremony requests it', () => {
const separated = request({ decisionMode: 'separation_of_duty' });
assert.throws(() => approve(separated), ApprovalSeparationOfDutyError);
const decided = approve(separated, {
principal: principal(REVIEWER, {
authenticationId: 'auth-reviewer-1',
assurance: 'multi_factor',
}),
});
assert.equal(decided.decidedBy.id, REVIEWER.id);
assert.equal(decided.decisionAssurance, 'multi_factor');
});
test('rejects weak, service and expired human decision principals', () => {
for (const candidate of [
principal(REQUESTER, { assurance: 'single_factor' }),
principal(
{ type: 'system', id: 'not-human' },
{ assurance: 'service' },
),
principal(REQUESTER, { expiresAtMs: 2_000 }),
]) {
assert.throws(
() => approve(request(), { principal: candidate }),
ApprovalHumanDecisionRequiredError,
);
}
});
test('provides exact decision and consumption replay while rejecting drift', () => {
const decided = approve(request());
assert.deepEqual(approve(decided), decided);
assert.throws(
() => approve(decided, { reasonCode: 'changed' }),
ApprovalMutationConflictError,
);
const consumed = consume(decided);
const replay = consume(consumed.request);
assert.deepEqual(replay, consumed);
assert.throws(
() =>
consume(consumed.request, {
action: action({ previewDigest: 'c'.repeat(64) }),
}),
ApprovalMutationConflictError,
);
});
test('fails closed on expiry, stale versions and corrupt persisted tuples', () => {
assert.throws(
() =>
approve(request(), {
decidedAtMs: 61_000,
principal: principal(REQUESTER, { expiresAtMs: 70_000 }),
}),
ApprovalRequestExpiredError,
);
assert.throws(
() => approve(request(), { expectedVersion: 2 }),
ApprovalRequestVersionConflictError,
);
const decided = approve(request());
assert.throws(
() => consume(decided, { consumedAtMs: 61_000 }),
ApprovalRequestExpiredError,
);
assert.throws(
() =>
normalizeApprovalRequestRecord({
...decided,
decisionAuthenticationId: null,
}),
InvalidApprovedActionValueError,
);
assert.throws(
() =>
normalizeApprovedActionDispatchRecord({
...consume(decided).dispatch,
unexpected: true,
}),
InvalidApprovedActionValueError,
);
});