225 lines
7.7 KiB
TypeScript
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);
|
|
});
|
|
});
|