121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|