/** * 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; function makePool(): jest.Mocked { const pool = new MockPool() as jest.Mocked; pool.query = jest.fn(); return pool; } function makePolicyRow(overrides: Record = {}): Record { 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; 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: null, 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: null, 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: null, 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: null, 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: null, 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); }); }); });