/** * Integration tests for the ABE API server. * Uses supertest to hit the Express app directly (no real browser). * The SessionStore is given a mock that never calls Playwright. */ import request from 'supertest'; import { createApp } from '../../src/server/index'; import { SessionStore, SessionRecord } from '../../src/server/SessionStore'; import { IAnomaly } from '../../src/core/interfaces'; // ─── Mock SessionStore ──────────────────────────────────────────────────────── function makeAnomaly(overrides: Partial = {}): IAnomaly { return { id: 'anom_test1', type: 'http_error', severity: 'high', observationId: 'obs_1', actionTrace: [], description: 'HTTP 500 on form submit', evidence: {}, timestamp: 1000000, ...overrides, }; } function makeSession(overrides: Partial = {}): SessionRecord { return { sessionId: 'sess_1', url: 'http://localhost:3000', seed: 42, maxStates: 50, status: 'running', startedAt: '2026-01-01T00:00:00.000Z', statesVisited: 5, anomaliesFound: 1, anomalies: [makeAnomaly()], ...overrides, }; } class MockSessionStore extends SessionStore { private _sessions: SessionRecord[]; constructor(sessions: SessionRecord[] = []) { super('./reports'); this._sessions = sessions; } getAllSessions() { return this._sessions; } getSession(id: string) { return this._sessions.find((s) => s.sessionId === id); } getAllAnomalies(sessionId?: string, severity?: string) { return this._sessions .flatMap((s) => s.anomalies) .filter((a) => !sessionId || this.findSessionForAnomaly(a.id) === sessionId) .filter((a) => !severity || a.severity === severity); } getAnomaly(id: string) { return this._sessions.flatMap((s) => s.anomalies).find((a) => a.id === id); } findSessionForAnomaly(anomalyId: string) { return this._sessions.find((s) => s.anomalies.some((a) => a.id === anomalyId))?.sessionId; } screenshotPath(_anomalyId: string) { return undefined; } stopSession(id: string) { const s = this.getSession(id); if (!s || s.status !== 'running') return false; s.status = 'stopped'; return true; } async startSession(params: { url: string; seed: number; maxStates: number }) { const record = makeSession({ sessionId: `sess_new`, url: params.url, seed: params.seed, maxStates: params.maxStates, status: 'running', anomalies: [], anomaliesFound: 0, }); this._sessions.push(record); return record; } async replayAnomaly(anomalyId: string) { if (!this.getAnomaly(anomalyId)) throw new Error('Anomaly not found'); return `replay_${anomalyId}`; } } // ─── Tests ──────────────────────────────────────────────────────────────────── describe('GET /api/sessions', () => { it('returns empty array when no sessions', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).get('/api/sessions'); expect(res.status).toBe(200); expect(res.body).toEqual([]); }); it('returns list of sessions', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).get('/api/sessions'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].sessionId).toBe('sess_1'); expect(res.body[0].status).toBe('running'); }); }); describe('GET /api/sessions/:sessionId', () => { it('returns session detail', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).get('/api/sessions/sess_1'); expect(res.status).toBe(200); expect(res.body.sessionId).toBe('sess_1'); expect(res.body.seed).toBe(42); }); it('returns 404 for unknown session', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).get('/api/sessions/no_such'); expect(res.status).toBe(404); }); }); describe('POST /api/sessions', () => { it('creates a new session', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app) .post('/api/sessions') .send({ url: 'http://localhost:3000', seed: 1, maxStates: 10 }); expect(res.status).toBe(201); expect(res.body.sessionId).toBeDefined(); expect(res.body.status).toBe('running'); }); it('returns 400 when url is missing', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).post('/api/sessions').send({ seed: 1 }); expect(res.status).toBe(400); }); }); describe('DELETE /api/sessions/:sessionId', () => { it('stops a running session', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).delete('/api/sessions/sess_1'); expect(res.status).toBe(200); expect(res.body.stopped).toBe(true); }); it('returns 404 for unknown session', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).delete('/api/sessions/no_such'); expect(res.status).toBe(404); }); }); describe('GET /api/anomalies', () => { it('returns all anomalies', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).get('/api/anomalies'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].id).toBe('anom_test1'); expect(res.body[0].severity).toBe('high'); }); it('filters by severity', async () => { const session = makeSession({ anomalies: [ makeAnomaly({ id: 'anom_h', severity: 'high' }), makeAnomaly({ id: 'anom_l', severity: 'low' }), ], anomaliesFound: 2, }); const app = createApp(new MockSessionStore([session])); const res = await request(app).get('/api/anomalies?severity=low'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].id).toBe('anom_l'); }); }); describe('GET /api/anomalies/:anomalyId', () => { it('returns anomaly detail', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).get('/api/anomalies/anom_test1'); expect(res.status).toBe(200); expect(res.body.type).toBe('http_error'); expect(res.body.description).toBe('HTTP 500 on form submit'); }); it('returns 404 for unknown anomaly', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).get('/api/anomalies/no_such'); expect(res.status).toBe(404); }); }); describe('GET /api/anomalies/:anomalyId/screenshot', () => { it('returns 404 when no screenshot exists', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).get('/api/anomalies/anom_test1/screenshot'); expect(res.status).toBe(404); }); }); describe('POST /api/anomalies/:anomalyId/replay', () => { it('returns replayId and running status', async () => { const app = createApp(new MockSessionStore([makeSession()])); const res = await request(app).post('/api/anomalies/anom_test1/replay'); expect(res.status).toBe(200); expect(res.body.replayId).toBeDefined(); expect(res.body.status).toBe('running'); }); it('returns 404 for unknown anomaly', async () => { const app = createApp(new MockSessionStore([])); const res = await request(app).post('/api/anomalies/no_such/replay'); expect(res.status).toBe(404); }); });