/** * Integration tests for Audit Log Service endpoints. */ import crypto from 'crypto'; import request from 'supertest'; import { Application } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { Pool } from 'pg'; 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'; function makeToken(sub: string, scope: string = 'audit:read'): string { return signToken({ sub, client_id: sub, scope, jti: uuidv4() }, privateKey); } describe('Audit Log Service Integration Tests', () => { let app: Application; let pool: Pool; beforeAll(async () => { app = await createApp(); pool = new Pool({ connectionString: process.env['DATABASE_URL'] }); const migrations = [ `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(); }); async function insertAuditEvent( agentId: string, action: string = 'token.issued', outcome: string = 'success', ): Promise { const result = await pool.query( `INSERT INTO audit_events (event_id, agent_id, action, outcome, ip_address, user_agent, metadata) VALUES ($1, $2, $3, $4, '127.0.0.1', 'test/1.0', '{}') RETURNING event_id`, [uuidv4(), agentId, action, outcome], ); return result.rows[0].event_id; } describe('GET /api/v1/audit', () => { it('should return a paginated list of audit events', async () => { const agentId = uuidv4(); await insertAuditEvent(agentId); const token = makeToken(agentId); const res = await request(app) .get('/api/v1/audit') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.data).toBeInstanceOf(Array); expect(res.body.total).toBeGreaterThanOrEqual(1); }); it('should filter by agentId', async () => { const agentId = uuidv4(); await insertAuditEvent(agentId); await insertAuditEvent(uuidv4()); // different agent const token = makeToken(agentId); const res = await request(app) .get(`/api/v1/audit?agentId=${agentId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); res.body.data.forEach((e: { agentId: string }) => expect(e.agentId).toBe(agentId), ); }); it('should filter by action', async () => { const agentId = uuidv4(); await insertAuditEvent(agentId, 'token.issued'); await insertAuditEvent(agentId, 'auth.failed'); const token = makeToken(agentId); const res = await request(app) .get('/api/v1/audit?action=token.issued') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); res.body.data.forEach((e: { action: string }) => expect(e.action).toBe('token.issued'), ); }); it('should filter by outcome', async () => { const agentId = uuidv4(); await insertAuditEvent(agentId, 'token.issued', 'success'); await insertAuditEvent(agentId, 'auth.failed', 'failure'); const token = makeToken(agentId); const res = await request(app) .get('/api/v1/audit?outcome=failure') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); res.body.data.forEach((e: { outcome: string }) => expect(e.outcome).toBe('failure'), ); }); it('should return 401 without a token', async () => { const res = await request(app).get('/api/v1/audit'); expect(res.status).toBe(401); }); it('should return 403 without audit:read scope', async () => { const token = makeToken(uuidv4(), 'agents:read'); const res = await request(app) .get('/api/v1/audit') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(403); }); it('should return 400 for fromDate outside 90-day retention window', async () => { const token = makeToken(uuidv4()); const oldDate = new Date(); oldDate.setDate(oldDate.getDate() - 100); const res = await request(app) .get(`/api/v1/audit?fromDate=${oldDate.toISOString()}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(400); expect(res.body.code).toBe('RETENTION_WINDOW_EXCEEDED'); }); it('should apply default pagination', async () => { const token = makeToken(uuidv4()); const res = await request(app) .get('/api/v1/audit') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.page).toBe(1); expect(res.body.limit).toBe(50); }); }); describe('GET /api/v1/audit/:eventId', () => { it('should return a single audit event', async () => { const agentId = uuidv4(); const eventId = await insertAuditEvent(agentId); const token = makeToken(agentId); const res = await request(app) .get(`/api/v1/audit/${eventId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.eventId).toBe(eventId); }); it('should return 404 for unknown eventId', async () => { const token = makeToken(uuidv4()); const res = await request(app) .get(`/api/v1/audit/${uuidv4()}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(404); }); it('should return 403 without audit:read scope', async () => { const agentId = uuidv4(); const eventId = await insertAuditEvent(agentId); const token = makeToken(agentId, 'agents:read'); const res = await request(app) .get(`/api/v1/audit/${eventId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(403); }); }); });