Test fixes (type mismatches introduced by V&V resolution changes):
- HealthDetailedController.test.ts: replace pool/makePool with dbProbe/makeDbProbe
to match refactored HealthDetailedDeps interface (Pool → DbProbe abstraction)
- EventPublisher.test.ts: pass all 4 required constructor args to WebhookDeliveryWorker
mock (pool, vaultClient, redisClient, redisUrl) — was passing only 1
- MarketplaceService.test.ts: IAgent.did/didCreatedAt are string|undefined (not null);
fix makeAgent defaults and makeAgent({did:null}) call; fix type assertion to unknown first
- OIDCTrustPolicyService.test.ts: ICreateTrustPolicyRequest.branch is string|undefined
(not nullable); replace all branch:null with branch:undefined
Security fix:
- npm audit fix: lodash ≤4.17.23 (HIGH) → patched; 0 vulnerabilities remaining
Result: 50/50 test suites pass, 722/722 tests pass, 0 vulnerabilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
/**
|
|
* Unit tests for src/services/OIDCTrustPolicyService.ts
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import {
|
|
OIDCTrustPolicyService,
|
|
TrustPolicyNotFoundError,
|
|
TrustPolicyViolationError,
|
|
} from '../../../src/services/OIDCTrustPolicyService';
|
|
import { ValidationError } from '../../../src/utils/errors';
|
|
|
|
jest.mock('pg');
|
|
|
|
const MockPool = Pool as jest.MockedClass<typeof Pool>;
|
|
|
|
function makePool(): jest.Mocked<Pool> {
|
|
const pool = new MockPool() as jest.Mocked<Pool>;
|
|
pool.query = jest.fn();
|
|
return pool;
|
|
}
|
|
|
|
function makePolicyRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
return {
|
|
id: 'policy-001',
|
|
provider: 'github',
|
|
repository: 'acme/my-repo',
|
|
branch: null,
|
|
agent_id: 'agent-001',
|
|
created_at: new Date('2026-01-01'),
|
|
updated_at: new Date('2026-01-01'),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('OIDCTrustPolicyService', () => {
|
|
let service: OIDCTrustPolicyService;
|
|
let pool: jest.Mocked<Pool>;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
pool = makePool();
|
|
service = new OIDCTrustPolicyService(pool);
|
|
});
|
|
|
|
describe('createTrustPolicy()', () => {
|
|
it('should create a trust policy successfully', async () => {
|
|
pool.query = jest.fn()
|
|
.mockResolvedValueOnce({ rows: [{ agent_id: 'agent-001' }], rowCount: 1 })
|
|
.mockResolvedValueOnce({ rows: [makePolicyRow()], rowCount: 1 });
|
|
|
|
const result = await service.createTrustPolicy({
|
|
provider: 'github',
|
|
repository: 'acme/my-repo',
|
|
branch: undefined,
|
|
agentId: 'agent-001',
|
|
});
|
|
|
|
expect(result.provider).toBe('github');
|
|
expect(result.repository).toBe('acme/my-repo');
|
|
expect(result.branch).toBeNull();
|
|
});
|
|
|
|
it('should throw ValidationError for non-github provider', async () => {
|
|
await expect(
|
|
service.createTrustPolicy({
|
|
provider: 'gitlab' as never,
|
|
repository: 'acme/my-repo',
|
|
branch: undefined,
|
|
agentId: 'agent-001',
|
|
}),
|
|
).rejects.toThrow(ValidationError);
|
|
});
|
|
|
|
it('should throw ValidationError for malformed repository', async () => {
|
|
await expect(
|
|
service.createTrustPolicy({
|
|
provider: 'github',
|
|
repository: 'no-slash-here',
|
|
branch: undefined,
|
|
agentId: 'agent-001',
|
|
}),
|
|
).rejects.toThrow(ValidationError);
|
|
});
|
|
|
|
it('should throw ValidationError when agentId is empty', async () => {
|
|
await expect(
|
|
service.createTrustPolicy({
|
|
provider: 'github',
|
|
repository: 'acme/my-repo',
|
|
branch: undefined,
|
|
agentId: '',
|
|
}),
|
|
).rejects.toThrow(ValidationError);
|
|
});
|
|
|
|
it('should throw ValidationError when agent not found', async () => {
|
|
pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
|
|
|
await expect(
|
|
service.createTrustPolicy({
|
|
provider: 'github',
|
|
repository: 'acme/my-repo',
|
|
branch: undefined,
|
|
agentId: 'nonexistent',
|
|
}),
|
|
).rejects.toThrow(ValidationError);
|
|
});
|
|
});
|
|
|
|
describe('listTrustPoliciesForAgent()', () => {
|
|
it('should return mapped policies', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({
|
|
rows: [makePolicyRow(), makePolicyRow({ id: 'policy-002' })],
|
|
rowCount: 2,
|
|
});
|
|
|
|
const policies = await service.listTrustPoliciesForAgent('agent-001');
|
|
|
|
expect(policies).toHaveLength(2);
|
|
expect(policies[0].id).toBe('policy-001');
|
|
});
|
|
|
|
it('should return an empty array when no policies exist', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
|
|
|
|
const policies = await service.listTrustPoliciesForAgent('agent-001');
|
|
|
|
expect(policies).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('deleteTrustPolicy()', () => {
|
|
it('should delete a policy successfully', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({ rowCount: 1 });
|
|
|
|
await expect(service.deleteTrustPolicy('policy-001')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should throw TrustPolicyNotFoundError when policy does not exist', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({ rowCount: 0 });
|
|
|
|
await expect(service.deleteTrustPolicy('nonexistent')).rejects.toThrow(TrustPolicyNotFoundError);
|
|
});
|
|
});
|
|
|
|
describe('enforceTrustPolicy()', () => {
|
|
it('should pass when a wildcard branch policy exists (branch: null)', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({
|
|
rows: [makePolicyRow({ branch: null })],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(
|
|
service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should pass when branch matches exactly', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({
|
|
rows: [makePolicyRow({ branch: 'main' })],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(
|
|
service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should normalize refs/heads/ prefix and match', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({
|
|
rows: [makePolicyRow({ branch: 'main' })],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(
|
|
service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should throw TrustPolicyViolationError when no policies exist', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
|
|
|
|
await expect(
|
|
service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
|
|
).rejects.toThrow(TrustPolicyViolationError);
|
|
});
|
|
|
|
it('should throw TrustPolicyViolationError when branch does not match constrained policy', async () => {
|
|
pool.query = jest.fn().mockResolvedValue({
|
|
rows: [makePolicyRow({ branch: 'main' })],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(
|
|
service.enforceTrustPolicy('github', 'acme/my-repo', 'feature/evil', 'agent-001'),
|
|
).rejects.toThrow(TrustPolicyViolationError);
|
|
});
|
|
});
|
|
});
|