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 () => { await createScenario('paged-sc-a'); await createScenario('paged-sc-b'); const res = await request(app.getHttpServer()) .get('/scenarios?page=1&limit=1') .expect(200); expect(res.body.data).toHaveLength(1); expect(res.body.limit).toBe(1); expect(res.body.page).toBe(1); }); it('returns empty data for out-of-range page', async () => { const res = await request(app.getHttpServer()).get('/scenarios?page=9999&limit=20').expect(200); expect(res.body.data).toHaveLength(0); }); it('returns 400 for invalid pagination params', async () => { await request(app.getHttpServer()).get('/scenarios?page=0').expect(400); }); it('orders by name ASC', async () => { await createScenario('zzz-order-sc'); await createScenario('aaa-order-sc'); const res = await request(app.getHttpServer()).get('/scenarios?orderBy=name&orderDir=ASC').expect(200); const names: string[] = res.body.data.map((s: any) => s.name); expect(names).toEqual([...names].sort()); }); it('orders by name DESC', async () => { const res = await request(app.getHttpServer()).get('/scenarios?orderBy=name&orderDir=DESC').expect(200); const names: string[] = res.body.data.map((s: any) => s.name); expect(names).toEqual([...names].sort().reverse()); }); it('returns 400 for invalid orderDir', async () => { await request(app.getHttpServer()).get('/scenarios?orderDir=SIDEWAYS').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); }); }); // ── GET /scenarios/:id/export ───────────────────────────────────────────── describe('GET /scenarios/:id/export', () => { it('returns name and steps array', async () => { const sc = await createScenario('export-me'); await createStep(sc.id, { order: 0, type: 'login', sessionName: 's', execCode: '{"keyId":"k","environmentName":"e"}' }); await createStep(sc.id, { order: 1, type: 'exec', sessionName: 's', execCode: 'return 1;', validateCode: 'return true;' }); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); expect(res.body.name).toBe('export-me'); expect(Array.isArray(res.body.steps)).toBe(true); expect(res.body.steps).toHaveLength(2); }); it('exports steps ordered by order field', async () => { const sc = await createScenario('export-order'); await createStep(sc.id, { order: 2, sessionName: 's' }); await createStep(sc.id, { order: 0, sessionName: 's' }); await createStep(sc.id, { order: 1, sessionName: 's' }); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); const orders = res.body.steps.map((s: any) => s.order); expect(orders).toEqual([0, 1, 2]); }); it('omits internal fields (id, scenarioId, timestamps)', async () => { const sc = await createScenario('export-shape'); await createStep(sc.id, { order: 0, sessionName: 's' }); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); const step = res.body.steps[0]; expect(step).not.toHaveProperty('id'); expect(step).not.toHaveProperty('scenarioId'); expect(step).not.toHaveProperty('createdAt'); expect(step).not.toHaveProperty('updatedAt'); }); it('exports null validateCode as null', async () => { const sc = await createScenario('export-null-validate'); await createStep(sc.id, { order: 0, sessionName: 's', execCode: 'return 1;' }); const res = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); expect(res.body.steps[0].validateCode).toBeNull(); }); it('returns 404 for unknown scenario', async () => { await request(app.getHttpServer()).get('/scenarios/99999/export').expect(404); }); }); // ── POST /scenarios/import ──────────────────────────────────────────────── describe('POST /scenarios/import', () => { it('creates a new scenario with all steps', async () => { const payload = { name: 'imported scenario', steps: [ { order: 0, type: 'login', sessionName: 's', execCode: '{"keyId":"k","environmentName":"e"}', validateCode: null }, { order: 1, type: 'exec', sessionName: 's', execCode: 'return 1;', validateCode: 'return true;' }, { order: 2, type: 'sign', sessionName: 's', execCode: '{"keyId":"k"}', validateCode: null }, ], }; const res = await request(app.getHttpServer()) .post('/scenarios/import') .send(payload) .expect(201); expect(res.body.id).toBeDefined(); expect(res.body.name).toBe('imported scenario'); expect(res.body.steps).toHaveLength(3); expect(res.body.steps[0].type).toBe('login'); expect(res.body.steps[1].type).toBe('exec'); expect(res.body.steps[2].type).toBe('sign'); }); it('assigns a new id (does not collide with source)', async () => { const sc = await createScenario('original'); const exportRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); const importRes = await request(app.getHttpServer()) .post('/scenarios/import') .send(exportRes.body) .expect(201); expect(importRes.body.id).not.toBe(sc.id); }); it('round-trips a scenario faithfully', async () => { const sc = await createScenario('roundtrip'); await createStep(sc.id, { order: 0, type: 'exec', sessionName: 'rs', execCode: 'return 42;', validateCode: 'return true;' }); const exportRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) .expect(200); const importRes = await request(app.getHttpServer()) .post('/scenarios/import') .send(exportRes.body) .expect(201); expect(importRes.body.name).toBe('roundtrip'); expect(importRes.body.steps).toHaveLength(1); expect(importRes.body.steps[0].execCode).toBe(exportRes.body.steps[0].execCode); expect(importRes.body.steps[0].validateCode).toBe(exportRes.body.steps[0].validateCode); }); it('imports with empty steps array', async () => { const res = await request(app.getHttpServer()) .post('/scenarios/import') .send({ name: 'empty-import', steps: [] }) .expect(201); expect(res.body.name).toBe('empty-import'); expect(res.body.steps).toHaveLength(0); }); it('returns 400 when name is missing', async () => { await request(app.getHttpServer()) .post('/scenarios/import') .send({ steps: [] }) .expect(400); }); it('returns 400 when steps is not an array', async () => { await request(app.getHttpServer()) .post('/scenarios/import') .send({ name: 'bad', steps: 'oops' }) .expect(400); }); it('returns 400 when a step has an invalid type', async () => { await request(app.getHttpServer()) .post('/scenarios/import') .send({ name: 'bad-type', steps: [{ order: 0, type: 'unknown', sessionName: 's' }] }) .expect(400); }); }); });