/** * Integration tests for Agent Registry endpoints. * Uses a real Postgres test DB and Redis test instance. */ import crypto from 'crypto'; import request from 'supertest'; import { Application } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { Pool } from 'pg'; import { createClient } from 'redis'; // Set test environment variables before importing app const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); process.env['DATABASE_URL'] = process.env['TEST_DATABASE_URL'] ?? 'postgresql://sentryagent:sentryagent@localhost:5432/sentryagent_idp_test'; process.env['REDIS_URL'] = process.env['TEST_REDIS_URL'] ?? 'redis://localhost:6379/1'; process.env['JWT_PRIVATE_KEY'] = privateKey; process.env['JWT_PUBLIC_KEY'] = publicKey; process.env['NODE_ENV'] = 'test'; import { createApp } from '../../src/app'; import { signToken } from '../../src/utils/jwt'; import { closePool } from '../../src/db/pool'; import { closeRedisClient } from '../../src/cache/redis'; const AGENT_ID = uuidv4(); const SCOPE = 'agents:read agents:write'; function makeToken(sub: string = AGENT_ID, scope: string = SCOPE): string { return signToken({ sub, client_id: sub, scope, jti: uuidv4() }, privateKey); } describe('Agent Registry Integration Tests', () => { let app: Application; let pool: Pool; beforeAll(async () => { app = await createApp(); pool = new Pool({ connectionString: process.env['DATABASE_URL'], }); // Run migrations const migrations = [ `CREATE TABLE IF NOT EXISTS schema_migrations (name VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`, `CREATE TABLE IF NOT EXISTS agents ( agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, agent_type VARCHAR(32) NOT NULL, version VARCHAR(64) NOT NULL, capabilities TEXT[] NOT NULL DEFAULT '{}', owner VARCHAR(128) NOT NULL, deployment_env VARCHAR(16) NOT NULL, status VARCHAR(24) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS credentials ( credential_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), client_id UUID NOT NULL, secret_hash VARCHAR(255) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ )`, `CREATE TABLE IF NOT EXISTS audit_events ( event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id UUID NOT NULL, action VARCHAR(32) NOT NULL, outcome VARCHAR(16) NOT NULL, ip_address VARCHAR(64) NOT NULL, user_agent TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{}', timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS token_revocations ( jti UUID PRIMARY KEY, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, ]; for (const sql of migrations) { await pool.query(sql); } }); afterEach(async () => { await pool.query('DELETE FROM audit_events'); await pool.query('DELETE FROM credentials'); await pool.query('DELETE FROM agents'); }); afterAll(async () => { await pool.end(); await closePool(); await closeRedisClient(); }); const validAgent = { email: 'test-agent@sentryagent.ai', agentType: 'screener', version: '1.0.0', capabilities: ['resume:read'], owner: 'test-team', deploymentEnv: 'development', }; describe('POST /api/v1/agents', () => { it('should register a new agent and return 201', async () => { const token = makeToken(); const res = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); expect(res.status).toBe(201); expect(res.body.agentId).toBeDefined(); expect(res.body.email).toBe(validAgent.email); expect(res.body.status).toBe('active'); }); it('should return 401 without a token', async () => { const res = await request(app).post('/api/v1/agents').send(validAgent); expect(res.status).toBe(401); }); it('should return 400 for invalid request body', async () => { const token = makeToken(); const res = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send({ email: 'not-an-email' }); expect(res.status).toBe(400); }); it('should return 409 for duplicate email', async () => { const token = makeToken(); await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); expect(res.status).toBe(409); expect(res.body.code).toBe('AGENT_ALREADY_EXISTS'); }); }); describe('GET /api/v1/agents', () => { it('should return a paginated list of agents', async () => { const token = makeToken(); await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .get('/api/v1/agents') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.data).toBeInstanceOf(Array); expect(res.body.total).toBeGreaterThanOrEqual(1); expect(res.body.page).toBe(1); }); it('should support filtering by status', async () => { const token = makeToken(); await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .get('/api/v1/agents?status=active') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); res.body.data.forEach((a: { status: string }) => expect(a.status).toBe('active')); }); it('should return 401 without a token', async () => { const res = await request(app).get('/api/v1/agents'); expect(res.status).toBe(401); }); }); describe('GET /api/v1/agents/:agentId', () => { it('should return an agent by ID', async () => { const token = makeToken(); const created = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .get(`/api/v1/agents/${created.body.agentId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.agentId).toBe(created.body.agentId); }); it('should return 404 for unknown agentId', async () => { const token = makeToken(); const res = await request(app) .get(`/api/v1/agents/${uuidv4()}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(404); }); }); describe('PATCH /api/v1/agents/:agentId', () => { it('should update the agent and return 200', async () => { const token = makeToken(); const created = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .patch(`/api/v1/agents/${created.body.agentId}`) .set('Authorization', `Bearer ${token}`) .send({ version: '2.0.0' }); expect(res.status).toBe(200); expect(res.body.version).toBe('2.0.0'); }); it('should return 404 for unknown agentId', async () => { const token = makeToken(); const res = await request(app) .patch(`/api/v1/agents/${uuidv4()}`) .set('Authorization', `Bearer ${token}`) .send({ version: '2.0.0' }); expect(res.status).toBe(404); }); }); describe('DELETE /api/v1/agents/:agentId', () => { it('should decommission the agent and return 204', async () => { const token = makeToken(); const created = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); const res = await request(app) .delete(`/api/v1/agents/${created.body.agentId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(204); }); it('should return 409 if already decommissioned', async () => { const token = makeToken(); const created = await request(app) .post('/api/v1/agents') .set('Authorization', `Bearer ${token}`) .send(validAgent); await request(app) .delete(`/api/v1/agents/${created.body.agentId}`) .set('Authorization', `Bearer ${token}`); const res = await request(app) .delete(`/api/v1/agents/${created.body.agentId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(409); }); }); });