import { INestApplication } from '@nestjs/common'; import request from 'supertest'; import { buildTestApp } from './app.harness'; describe('ScenarioController', () => { let app: INestApplication; beforeAll(async () => { app = await buildTestApp(); }); afterAll(async () => { await app.close(); }); // ── helpers ──────────────────────────────────────────────────────────────── async function createScenario(name = 'test scenario') { const res = await request(app.getHttpServer()) .post('/scenarios') .send({ name }) .expect(201); return res.body as { id: number; name: string }; } async function createStep(scenarioId: number, overrides: Record = {}) { const res = await request(app.getHttpServer()) .post(`/scenarios/${scenarioId}/steps`) .send({ order: 0, type: 'exec', sessionName: 'test-session', execCode: 'return 1;', ...overrides, }) .expect(201); return res.body as { id: number }; } // ── POST /scenarios ──────────────────────────────────────────────────────── describe('POST /scenarios', () => { it('creates a scenario and returns 201', async () => { const res = await request(app.getHttpServer()) .post('/scenarios') .send({ name: 'my scenario' }) .expect(201); expect(res.body.id).toBeDefined(); expect(res.body.name).toBe('my scenario'); }); it('returns 400 when name is missing', async () => { await request(app.getHttpServer()).post('/scenarios').send({}).expect(400); }); }); // ── GET /scenarios ───────────────────────────────────────────────────────── describe('GET /scenarios', () => { it('returns paginated result', async () => { const res = await request(app.getHttpServer()).get('/scenarios').expect(200); expect(res.body).toHaveProperty('data'); expect(res.body).toHaveProperty('total'); expect(res.body).toHaveProperty('page'); expect(res.body).toHaveProperty('limit'); expect(Array.isArray(res.body.data)).toBe(true); }); it('respects page and limit params', async () => { const res = await request(app.getHttpServer()) .get('/scenarios?page=1&limit=2') .expect(200); expect(res.body.limit).toBe(2); expect(res.body.page).toBe(1); }); it('returns 400 for invalid pagination params', async () => { await request(app.getHttpServer()).get('/scenarios?page=0').expect(400); }); }); // ── GET /scenarios/:id ───────────────────────────────────────────────────── describe('GET /scenarios/:id', () => { it('returns the scenario with steps array', async () => { const sc = await createScenario('scenario-get-one'); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}`) .expect(200); expect(res.body.id).toBe(sc.id); expect(Array.isArray(res.body.steps)).toBe(true); }); it('returns 404 for unknown id', async () => { await request(app.getHttpServer()).get('/scenarios/99999').expect(404); }); }); // ── PATCH /scenarios/:id ─────────────────────────────────────────────────── describe('PATCH /scenarios/:id', () => { it('updates scenario name', async () => { const sc = await createScenario('patch-me'); const res = await request(app.getHttpServer()) .patch(`/scenarios/${sc.id}`) .send({ name: 'patched' }) .expect(200); expect(res.body.name).toBe('patched'); }); it('returns 404 for unknown id', async () => { await request(app.getHttpServer()) .patch('/scenarios/99999') .send({ name: 'x' }) .expect(404); }); }); // ── DELETE /scenarios/:id ────────────────────────────────────────────────── describe('DELETE /scenarios/:id', () => { it('deletes and returns 204', async () => { const sc = await createScenario('delete-me'); await request(app.getHttpServer()).delete(`/scenarios/${sc.id}`).expect(204); await request(app.getHttpServer()).get(`/scenarios/${sc.id}`).expect(404); }); it('returns 404 for unknown id', async () => { await request(app.getHttpServer()).delete('/scenarios/99999').expect(404); }); }); // ── POST /scenarios/:id/steps ────────────────────────────────────────────── describe('POST /scenarios/:id/steps', () => { it('creates a step with required fields', async () => { const sc = await createScenario(); const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, type: 'exec', sessionName: 'my-session', execCode: 'return 1;' }) .expect(201); expect(res.body.id).toBeDefined(); expect(res.body.order).toBe(0); expect(res.body.type).toBe('exec'); expect(res.body.sessionName).toBe('my-session'); }); it('creates a login step', async () => { const sc = await createScenario(); const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, type: 'login', sessionName: 'session-x' }) .expect(201); expect(res.body.type).toBe('login'); }); it('returns 400 when order is missing', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ type: 'exec', sessionName: 'x' }) .expect(400); }); it('returns 400 when type is invalid', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, type: 'unknown', sessionName: 'x' }) .expect(400); }); it('returns 400 when sessionName is missing', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, type: 'exec' }) .expect(400); }); it('returns 404 for unknown scenario', async () => { await request(app.getHttpServer()) .post('/scenarios/99999/steps') .send({ order: 0, type: 'exec', sessionName: 'x' }) .expect(404); }); it('returns steps ordered by order field', async () => { const sc = await createScenario(); await createStep(sc.id, { order: 2, type: 'exec', sessionName: 's' }); await createStep(sc.id, { order: 0, type: 'exec', sessionName: 's' }); await createStep(sc.id, { order: 1, type: 'exec', sessionName: 's' }); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}`) .expect(200); const orders = res.body.steps.map((s: any) => s.order); expect(orders).toEqual([0, 1, 2]); }); }); // ── GET /scenarios/:id/steps/:stepId ────────────────────────────────────── describe('GET /scenarios/:id/steps/:stepId', () => { it('returns the step', async () => { const sc = await createScenario(); const step = await createStep(sc.id); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/steps/${step.id}`) .expect(200); expect(res.body.id).toBe(step.id); }); it('returns 404 for unknown step', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/steps/99999`) .expect(404); }); }); // ── PATCH /scenarios/:id/steps/:stepId ──────────────────────────────────── describe('PATCH /scenarios/:id/steps/:stepId', () => { it('updates step fields', async () => { const sc = await createScenario(); const step = await createStep(sc.id, { order: 0, execCode: 'return 1;' }); const res = await request(app.getHttpServer()) .patch(`/scenarios/${sc.id}/steps/${step.id}`) .send({ order: 5, execCode: 'return 99;' }) .expect(200); expect(res.body.order).toBe(5); expect(res.body.execCode).toBe('return 99;'); }); it('returns 404 for unknown step', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .patch(`/scenarios/${sc.id}/steps/99999`) .send({ order: 1 }) .expect(404); }); }); // ── DELETE /scenarios/:id/steps/:stepId ─────────────────────────────────── describe('DELETE /scenarios/:id/steps/:stepId', () => { it('deletes the step and returns 204', async () => { const sc = await createScenario(); const step = await createStep(sc.id); await request(app.getHttpServer()) .delete(`/scenarios/${sc.id}/steps/${step.id}`) .expect(204); await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/steps/${step.id}`) .expect(404); }); }); // ── POST /scenarios/:id/run ──────────────────────────────────────────────── describe('POST /scenarios/:id/run', () => { it('creates a run with stepRuns in correct initial states', async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 1 }); await createStep(sc.id, { order: 2 }); const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/run`) .expect(201); expect(res.body.status).toBe('pending'); expect(Array.isArray(res.body.stepRuns)).toBe(true); expect(res.body.stepRuns).toHaveLength(3); const statuses = res.body.stepRuns.map((s: any) => s.status); expect(statuses[0]).toBe('pending'); expect(statuses[1]).toBe('waiting'); expect(statuses[2]).toBe('waiting'); }); it('returns 404 for unknown scenario', async () => { await request(app.getHttpServer()) .post('/scenarios/99999/run') .expect(404); }); }); // ── GET /scenarios/:id/runs ──────────────────────────────────────────────── describe('GET /scenarios/:id/runs', () => { it('returns paginated runs with stepRuns embedded', async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); await request(app.getHttpServer()).post(`/scenarios/${sc.id}/run`).expect(201); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs`) .expect(200); expect(res.body.total).toBeGreaterThanOrEqual(1); expect(Array.isArray(res.body.data[0].stepRuns)).toBe(true); }); it('filters by status', async () => { const sc = await createScenario(); await createStep(sc.id, { order: 0 }); await request(app.getHttpServer()).post(`/scenarios/${sc.id}/run`).expect(201); const pendingRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs?status=pending`) .expect(200); expect(pendingRes.body.total).toBeGreaterThanOrEqual(1); pendingRes.body.data.forEach((r: any) => expect(r.status).toBe('pending')); const passRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs?status=pass`) .expect(200); expect(passRes.body.total).toBe(0); }); it('returns 400 for invalid status filter', async () => { const sc = await createScenario(); await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/runs?status=invalid`) .expect(400); }); it('returns 404 for unknown scenario', async () => { await request(app.getHttpServer()).get('/scenarios/99999/runs').expect(404); }); }); });