/** * API smoke tests for UAT / production readiness. * Tests health, routing, and auth validation without requiring full DB/Redis in CI. * * Run: npm test -- smoke */ import request from 'supertest'; // Load app without starting server (server.ts is not imported) // Suppress DB config console logs in test const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = process.env.NODE_ENV || 'test'; let app: import('express').Application; beforeAll(() => { app = require('../../app').default; }); afterAll(() => { process.env.NODE_ENV = originalEnv; }); describe('API Smoke – Health & Routing', () => { it('SMK-01: GET /health returns 200 and status OK', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('status', 'OK'); expect(res.body).toHaveProperty('timestamp'); expect(res.body).toHaveProperty('uptime'); }); it('SMK-02: GET /api/v1/health returns 200 and service name', async () => { const res = await request(app).get('/api/v1/health'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('status', 'OK'); expect(res.body).toHaveProperty('service', 're-workflow-backend'); }); it('SMK-03: GET /api/v1/health/db returns 200 (connected) or 503 (disconnected)', async () => { const res = await request(app).get('/api/v1/health/db'); expect([200, 503]).toContain(res.status); if (res.status === 200) { expect(res.body).toHaveProperty('database', 'connected'); } else { expect(res.body).toHaveProperty('database', 'disconnected'); } }); it('SMK-04: Invalid API route returns 404 with JSON', async () => { const res = await request(app).get('/api/v1/invalid-route-xyz'); expect(res.status).toBe(404); expect(res.body).toHaveProperty('success', false); expect(res.body).toHaveProperty('message'); }); }); describe('API Smoke – Authentication', () => { it('AUTH-01: POST /api/v1/auth/sso-callback with empty body returns 400', async () => { const res = await request(app) .post('/api/v1/auth/sso-callback') .send({}) .set('Content-Type', 'application/json'); expect(res.status).toBe(400); expect(res.body).toHaveProperty('success', false); // Auth route validator returns "Request body validation failed"; legacy app route returns "email and oktaSub required" expect(res.body.message).toMatch(/email|oktaSub|required|validation failed/i); }); it('AUTH-02: POST /api/v1/auth/sso-callback without oktaSub returns 400', async () => { const res = await request(app) .post('/api/v1/auth/sso-callback') .send({ email: 'test@example.com' }) .set('Content-Type', 'application/json'); expect(res.status).toBe(400); expect(res.body).toHaveProperty('success', false); }); it('AUTH-03: POST /api/v1/auth/sso-callback without email returns 400', async () => { const res = await request(app) .post('/api/v1/auth/sso-callback') .send({ oktaSub: 'okta-123' }) .set('Content-Type', 'application/json'); expect(res.status).toBe(400); expect(res.body).toHaveProperty('success', false); }); it('AUTH-04: GET /api/v1/users without token returns 401', async () => { const res = await request(app).get('/api/v1/users'); expect(res.status).toBe(401); }); it('AUTH-05: GET /api/v1/users with invalid token returns 401', async () => { const res = await request(app) .get('/api/v1/users') .set('Authorization', 'Bearer invalid-token'); expect(res.status).toBe(401); }); }); describe('API Smoke – Security Headers (SEC-01, SEC-02, SEC-03)', () => { it('SEC-01: Response includes Content-Security-Policy header', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.headers).toHaveProperty('content-security-policy'); expect(res.headers['content-security-policy']).toBeTruthy(); }); it('SEC-02: Response includes X-Frame-Options (SAMEORIGIN or deny)', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.headers).toHaveProperty('x-frame-options'); expect(res.headers['x-frame-options'].toUpperCase()).toMatch(/SAMEORIGIN|DENY/); }); it('SEC-03: GET /metrics without admin auth returns 401', async () => { const res = await request(app).get('/metrics'); expect(res.status).toBe(401); }); });