Files
Autonomous-Bug-Explorer/tests/server/server.test.ts

225 lines
7.7 KiB
TypeScript

/**
* 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> = {}): 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> = {}): 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);
});
});