Files
iios/packages/iios-service/src/capability/capability.broker.spec.ts
T
maaz519 2223d7a891 feat(p9): CMP consent gate in the Capability Broker
Task S2.1: the broker now checks consent (cmp.checkPurpose) after the OPA gate +
obligations and before the provider — a DENY blocks the send (BLOCKED/CONSENT_DENIED,
provider never called); ALLOW/NOT_REQUIRED proceed, ALLOW carrying the consent
receipt. Contracts gain CapabilityRequest.purpose + CapabilityResult.consentReceiptRef.
FakeCmp is now purpose-aware (denyPurpose/allowOnly). 5 new tests: allow+receipt,
deny→blocked, not-required, purpose-aware, OPA-before-CMP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:23:49 +05:30

99 lines
4.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { makeFakePorts } from '@insignia/iios-testkit';
import { PolicyDeniedError, type CapabilityRequest } from '@insignia/iios-contracts';
import { CapabilityBroker } from './capability.broker';
import { CapabilityProviderRegistry } from './capability.registry';
import type { FakePorts } from '@insignia/iios-testkit';
// No DB — the broker is pure governance over an in-memory provider registry.
const req = (over: Partial<CapabilityRequest> = {}): CapabilityRequest => ({
capability: 'channel.send',
scopeId: 'scope1',
channelType: 'WEBHOOK',
target: 'user@x',
payload: { text: 'hello' },
idempotencyKey: 'k1',
...over,
});
const broker = (ports: FakePorts) => new CapabilityBroker(ports, new CapabilityProviderRegistry());
describe('CapabilityBroker (P9 — governed egress)', () => {
it('allow + no obligations → provider SENT', async () => {
const r = await broker(makeFakePorts()).invoke(req());
expect(r.outcome).toBe('SENT');
expect(r.providerRef).toContain('sim-WEBHOOK');
});
it('fails closed: opa deny → PolicyDeniedError, provider never called', async () => {
const ports = makeFakePorts();
ports.opa.deny('no egress');
await expect(broker(ports).invoke(req())).rejects.toBeInstanceOf(PolicyDeniedError);
});
it('DENY_OUTBOUND obligation → BLOCKED, provider never called', async () => {
const ports = makeFakePorts();
ports.opa.setDecision({ allow: true, obligations: [{ kind: 'DENY_OUTBOUND', reason: 'user opted out' }] });
const r = await broker(ports).invoke(req());
expect(r.outcome).toBe('BLOCKED');
expect(r.errorCode).toBe('DENY_OUTBOUND');
});
it('REDACT obligation → provider receives masked payload', async () => {
const ports = makeFakePorts();
ports.opa.setDecision({ allow: true, obligations: [{ kind: 'REDACT', target: 'text', reason: 'PII' }] });
const registry = new CapabilityProviderRegistry();
let seen: unknown;
registry.register({ name: 'spy', channelTypes: ['WEBHOOK'], capabilities: { canSend: true }, async send(r) { seen = r.payload; return { providerRef: 'spy-1', outcome: 'SENT' }; } });
const r = await new CapabilityBroker(ports, registry).invoke(req());
expect(r.outcome).toBe('SENT');
expect((seen as { text: string }).text).toBe('[REDACTED]');
});
it('unknown channel → fails closed (throws, no silent egress)', async () => {
await expect(broker(makeFakePorts()).invoke(req({ channelType: 'CARRIER_PIGEON' }))).rejects.toThrow();
});
// ── CMP consent-at-egress (P9 slice 2) ──────────────────────────
it('CMP allow → SENT and carries the consent receipt', async () => {
const ports = makeFakePorts();
ports.cmp.setStatus('ALLOW');
const r = await broker(ports).invoke(req());
expect(r.outcome).toBe('SENT');
expect(r.consentReceiptRef).toBe('fake-receipt');
});
it('CMP DENY → BLOCKED (CONSENT_DENIED), provider never called', async () => {
const ports = makeFakePorts();
ports.cmp.setStatus('DENY');
const registry = new CapabilityProviderRegistry();
let called = false;
registry.register({ name: 'spy', channelTypes: ['WEBHOOK'], capabilities: { canSend: true }, async send() { called = true; return { providerRef: 'x', outcome: 'SENT' }; } });
const r = await new CapabilityBroker(ports, registry).invoke(req());
expect(r.outcome).toBe('BLOCKED');
expect(r.errorCode).toBe('CONSENT_DENIED');
expect(called).toBe(false);
});
it('CMP NOT_REQUIRED → SENT (no receipt required)', async () => {
const r = await broker(makeFakePorts()).invoke(req()); // default fake is NOT_REQUIRED
expect(r.outcome).toBe('SENT');
expect(r.consentReceiptRef).toBeUndefined();
});
it('purpose-aware: deny "marketing" but allow "support" for the same target', async () => {
const ports = makeFakePorts();
ports.cmp.denyPurpose('marketing');
const blocked = await broker(ports).invoke(req({ purpose: 'marketing' }));
const sent = await broker(ports).invoke(req({ purpose: 'support', idempotencyKey: 'k2' }));
expect(blocked.outcome).toBe('BLOCKED');
expect(sent.outcome).toBe('SENT');
});
it('OPA is consulted before CMP (policy deny short-circuits consent)', async () => {
const ports = makeFakePorts();
ports.opa.deny('not allowed');
ports.cmp.setStatus('ALLOW');
await expect(broker(ports).invoke(req())).rejects.toBeInstanceOf(PolicyDeniedError);
});
});