/** * Integration tests for OIDC Token Exchange endpoint. * Uses a real Postgres test DB and Redis test instance. * * Routes covered: * POST /api/v1/oidc/token — exchange a GitHub OIDC JWT for a SentryAgent.ai access token * * This is an unauthenticated endpoint — the GitHub OIDC JWT is the credential. * Trust-policy enforcement requires a matching oidc_trust_policies record. */ 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'; process.env['DEFAULT_ORG_ID'] = 'org_system'; import { createApp } from '../../src/app'; import { closePool } from '../../src/db/pool'; import { closeRedisClient } from '../../src/cache/redis'; describe('OIDC Token Exchange Endpoint Integration Tests', () => { let app: Application; let pool: Pool; beforeAll(async () => { app = await createApp(); pool = new Pool({ connectionString: process.env['DATABASE_URL'] }); await pool.query(` CREATE TABLE IF NOT EXISTS oidc_trust_policies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), provider VARCHAR(20) NOT NULL, repository VARCHAR(255) NOT NULL, branch VARCHAR(100), agent_id VARCHAR(40) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); }); afterAll(async () => { await pool.end(); await closePool(); await closeRedisClient(); }); // ─── POST /oidc/token ──────────────────────────────────────────────────────── describe('POST /api/v1/oidc/token', () => { it('should return 400 when request body is missing', async () => { const res = await request(app) .post('/api/v1/oidc/token') .send({}); expect([400, 422]).toContain(res.status); }); it('should return 400 when provider is missing', async () => { const res = await request(app) .post('/api/v1/oidc/token') .send({ token: 'fake-jwt', agentId: uuidv4() }); expect([400, 422]).toContain(res.status); }); it('should return 400 when token is missing', async () => { const res = await request(app) .post('/api/v1/oidc/token') .send({ provider: 'github', agentId: uuidv4() }); expect([400, 422]).toContain(res.status); }); it('should return 401 or 403 for an invalid GitHub OIDC token', async () => { // A malformed JWT will fail verification — the endpoint should reject it const res = await request(app) .post('/api/v1/oidc/token') .send({ provider: 'github', token: 'eyJhbGciOiJSUzI1NiJ9.invalid.payload', agentId: uuidv4(), scope: 'agents:read', }); expect([400, 401, 403, 422]).toContain(res.status); }); it('should return 403 when no trust policy matches the repository', async () => { // Build a minimally valid JWT structure with github claims (but won't pass GitHub JWKS) // The endpoint will reject after trust-policy lookup fails const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url'); const claims = { iss: 'https://token.actions.githubusercontent.com', sub: 'repo:nonexistent/repo:ref:refs/heads/main', repository: 'nonexistent/repo', ref: 'refs/heads/main', aud: 'sentryagent.ai', }; const payload = Buffer.from(JSON.stringify(claims)).toString('base64url'); const fakeJwt = `${header}.${payload}.fakesig`; const res = await request(app) .post('/api/v1/oidc/token') .send({ provider: 'github', token: fakeJwt, agentId: uuidv4(), scope: 'agents:read', }); // Either 401 (invalid JWT signature) or 403 (trust policy violation) expect([400, 401, 403]).toContain(res.status); }); }); });