- add PaginationQueryDto<TOrderBy> generic with orderBy and orderDir fields - add SessionOrderBy, EnvironmentOrderBy, ScenarioOrderBy type aliases - update session, environment, scenario services and controllers to use typed pagination - update MCP list_sessions, list_environments, list_scenarios tools to expose orderBy/orderDir - update tests for all list endpoints to cover page, limit, ordering, and invalid param rejection
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
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<string, unknown> = {}) {
|
|
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);
|
|
});
|
|
});
|
|
});
|