feat(phase-5): WS2 — A2A Authorization
Implements agent-to-agent delegation chains: - Migration 024: delegation_chains table with HMAC signature, TTL, revocation - DelegationCrypto: HMAC-SHA256 sign/verify, UUID token generation - DelegationService: create (scope subset validation, self-delegation guard, same-tenant delegatee check), verify (returns valid: false on expired/revoked, never throws), revoke (delegator-only, conflict guard) - DelegationController + router at /oauth2/token/delegate (POST/DELETE) and /oauth2/token/verify-delegation (POST) - Feature-flagged behind A2A_ENABLED env var (default on) - Prometheus metrics: delegations_created/verified/revoked_total - 33 tests (unit + integration): all pass, DelegationService 87.5%+ branch coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
tests/unit/utils/delegationCrypto.test.ts
Normal file
89
tests/unit/utils/delegationCrypto.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Unit tests for src/utils/delegationCrypto.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
signDelegationPayload,
|
||||
verifyDelegationSignature,
|
||||
generateDelegationToken,
|
||||
} from '../../../src/utils/delegationCrypto';
|
||||
import { DelegationTokenPayload } from '../../../src/types/delegation';
|
||||
|
||||
const MOCK_PAYLOAD: DelegationTokenPayload = {
|
||||
chainId: 'chain-uuid-1234',
|
||||
tenantId: 'org_system',
|
||||
delegatorAgentId: 'delegator-uuid',
|
||||
delegateeAgentId: 'delegatee-uuid',
|
||||
scopes: ['agents:read', 'tokens:read'],
|
||||
issuedAt: '2026-04-03T00:00:00.000Z',
|
||||
expiresAt: '2026-04-03T01:00:00.000Z',
|
||||
};
|
||||
|
||||
const SECRET = 'test-hmac-secret';
|
||||
|
||||
describe('delegationCrypto', () => {
|
||||
describe('signDelegationPayload', () => {
|
||||
it('returns a non-empty hex string', () => {
|
||||
const sig = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
expect(typeof sig).toBe('string');
|
||||
expect(sig.length).toBeGreaterThan(0);
|
||||
// SHA-256 hex = 64 chars
|
||||
expect(sig).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it('produces the same signature for the same payload and secret', () => {
|
||||
const sig1 = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
const sig2 = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
expect(sig1).toBe(sig2);
|
||||
});
|
||||
|
||||
it('produces different signatures for different secrets', () => {
|
||||
const sig1 = signDelegationPayload(MOCK_PAYLOAD, 'secret-a');
|
||||
const sig2 = signDelegationPayload(MOCK_PAYLOAD, 'secret-b');
|
||||
expect(sig1).not.toBe(sig2);
|
||||
});
|
||||
|
||||
it('produces different signatures for different payloads', () => {
|
||||
const payload2 = { ...MOCK_PAYLOAD, chainId: 'different-chain-uuid' };
|
||||
const sig1 = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
const sig2 = signDelegationPayload(payload2, SECRET);
|
||||
expect(sig1).not.toBe(sig2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyDelegationSignature', () => {
|
||||
it('returns true for a valid sign/verify round-trip', () => {
|
||||
const signature = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
expect(verifyDelegationSignature(MOCK_PAYLOAD, signature, SECRET)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the payload has been tampered with', () => {
|
||||
const signature = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
const tampered = { ...MOCK_PAYLOAD, scopes: ['admin:orgs'] };
|
||||
expect(verifyDelegationSignature(tampered, signature, SECRET)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when the secret does not match', () => {
|
||||
const signature = signDelegationPayload(MOCK_PAYLOAD, SECRET);
|
||||
expect(verifyDelegationSignature(MOCK_PAYLOAD, signature, 'wrong-secret')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an empty signature string', () => {
|
||||
expect(verifyDelegationSignature(MOCK_PAYLOAD, '', SECRET)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDelegationToken', () => {
|
||||
it('returns a UUID v4 string', () => {
|
||||
const token = generateDelegationToken();
|
||||
expect(token).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('generates unique tokens on each call', () => {
|
||||
const tokens = new Set(Array.from({ length: 50 }, () => generateDelegationToken()));
|
||||
expect(tokens.size).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user