- return scenario export payloads as yaml to match existing workflows - harden step reordering flow for drag-and-drop and one-based ui labels - switch server container to workspace-lockfile installs and root ignore rules
805 lines
27 KiB
TypeScript
805 lines
27 KiB
TypeScript
import { INestApplication } from "@nestjs/common";
|
|
import request from "supertest";
|
|
import { DataSource } from "typeorm";
|
|
import { parse as yamlParse } from "yaml";
|
|
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: string; name: string };
|
|
}
|
|
|
|
async function createStep(
|
|
scenarioId: string,
|
|
overrides: Record<string, unknown> = {},
|
|
) {
|
|
const res = await request(app.getHttpServer())
|
|
.post(`/scenarios/${scenarioId}/steps`)
|
|
.send({
|
|
order: 0,
|
|
sessionName: "test-session",
|
|
execCode: "return 1;",
|
|
...overrides,
|
|
})
|
|
.expect(201);
|
|
return res.body as { id: string };
|
|
}
|
|
|
|
// ── 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: { name: string }) => 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: { name: string }) => 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/00000000-0000-0000-0000-000000000001").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/00000000-0000-0000-0000-000000000001")
|
|
.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/00000000-0000-0000-0000-000000000001").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,
|
|
sessionName: "my-session",
|
|
execCode: "return 1;",
|
|
})
|
|
.expect(201);
|
|
|
|
expect(res.body.id).toBeDefined();
|
|
expect(res.body.order).toBe(0);
|
|
expect(res.body.sessionName).toBe("my-session");
|
|
});
|
|
|
|
it("creates a step without execCode", async () => {
|
|
const sc = await createScenario();
|
|
const res = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/steps`)
|
|
.send({ order: 0, sessionName: "session-x" })
|
|
.expect(201);
|
|
|
|
expect(res.body.sessionName).toBe("session-x");
|
|
expect(res.body.execCode).toBeNull();
|
|
});
|
|
|
|
it("allows creating a step without explicit order", async () => {
|
|
const sc = await createScenario();
|
|
await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/steps`)
|
|
.send({ sessionName: "x" })
|
|
.expect(201);
|
|
});
|
|
|
|
it("ignores unknown fields in payload", async () => {
|
|
const sc = await createScenario();
|
|
const res = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/steps`)
|
|
.send({ order: 0, type: "unknown", sessionName: "x" })
|
|
.expect(201);
|
|
|
|
expect(res.body).not.toHaveProperty("type");
|
|
});
|
|
|
|
it("allows missing sessionName", async () => {
|
|
const sc = await createScenario();
|
|
await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/steps`)
|
|
.send({ order: 0, execCode: "return 1;" })
|
|
.expect(201);
|
|
});
|
|
|
|
it("returns 404 for unknown scenario", async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/scenarios/00000000-0000-0000-0000-000000000001/steps")
|
|
.send({ order: 0, 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: { order: number }) => 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/00000000-0000-0000-0000-000000000001`)
|
|
.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(0);
|
|
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/00000000-0000-0000-0000-000000000001`)
|
|
.send({ order: 1 })
|
|
.expect(404);
|
|
});
|
|
|
|
it("reorders by exact target index", async () => {
|
|
const sc = await createScenario();
|
|
const stepA = await createStep(sc.id, { title: "A" });
|
|
const stepB = await createStep(sc.id, { title: "B" });
|
|
const stepC = await createStep(sc.id, { title: "C" });
|
|
const stepD = await createStep(sc.id, { title: "D" });
|
|
|
|
await request(app.getHttpServer())
|
|
.patch(`/scenarios/${sc.id}/steps/${stepA.id}`)
|
|
.send({ order: 2 })
|
|
.expect(200);
|
|
|
|
let res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}`)
|
|
.expect(200);
|
|
expect(
|
|
res.body.steps.map((s: { title: string }) => s.title),
|
|
).toEqual(["B", "C", "A", "D"]);
|
|
expect(
|
|
res.body.steps.map((s: { order: number }) => s.order),
|
|
).toEqual([0, 1, 2, 3]);
|
|
|
|
await request(app.getHttpServer())
|
|
.patch(`/scenarios/${sc.id}/steps/${stepD.id}`)
|
|
.send({ order: 0 })
|
|
.expect(200);
|
|
|
|
res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}`)
|
|
.expect(200);
|
|
expect(
|
|
res.body.steps.map((s: { title: string }) => s.title),
|
|
).toEqual(["D", "B", "C", "A"]);
|
|
expect(
|
|
res.body.steps.map((s: { order: number }) => s.order),
|
|
).toEqual([0, 1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
// ── 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: { status: string }) => 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/00000000-0000-0000-0000-000000000001/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: { status: string }) =>
|
|
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/00000000-0000-0000-0000-000000000001/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,
|
|
sessionName: "s",
|
|
execCode: '{"keyId":"k","environmentName":"e"}',
|
|
});
|
|
await createStep(sc.id, {
|
|
order: 1,
|
|
sessionName: "s",
|
|
execCode: "return 1;",
|
|
validateCode: "return true;",
|
|
});
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/export`)
|
|
.expect(200);
|
|
|
|
const exported = yamlParse(res.text) as {
|
|
name: string;
|
|
steps: Array<Record<string, unknown>>;
|
|
};
|
|
|
|
expect(exported.name).toBe("export-me");
|
|
expect(Array.isArray(exported.steps)).toBe(true);
|
|
expect(exported.steps).toHaveLength(2);
|
|
});
|
|
|
|
it("exports steps ordered by sequential position", async () => {
|
|
const sc = await createScenario("export-order");
|
|
await createStep(sc.id, { sessionName: "s", execCode: "return 'first';" });
|
|
await createStep(sc.id, { sessionName: "s", execCode: "return 'second';" });
|
|
await createStep(sc.id, { sessionName: "s", execCode: "return 'third';" });
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/export`)
|
|
.expect(200);
|
|
|
|
const exported = yamlParse(res.text) as {
|
|
steps: Array<{ execCode: string }>;
|
|
};
|
|
|
|
const codes = exported.steps.map((s: { execCode: string }) => s.execCode);
|
|
expect(codes).toEqual([
|
|
"return 'first';",
|
|
"return 'second';",
|
|
"return 'third';",
|
|
]);
|
|
expect(exported.steps[0]).not.toHaveProperty("order");
|
|
});
|
|
|
|
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 exported = yamlParse(res.text) as {
|
|
steps: Array<Record<string, unknown>>;
|
|
};
|
|
|
|
const step = exported.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);
|
|
|
|
const exported = yamlParse(res.text) as {
|
|
steps: Array<{ validateCode: string | null }>;
|
|
};
|
|
|
|
expect(exported.steps[0].validateCode).toBeNull();
|
|
});
|
|
|
|
it("returns 404 for unknown scenario", async () => {
|
|
await request(app.getHttpServer())
|
|
.get("/scenarios/00000000-0000-0000-0000-000000000001/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: [
|
|
{
|
|
sessionName: "s",
|
|
execCode: '{"keyId":"k","environmentName":"e"}',
|
|
validateCode: null,
|
|
},
|
|
{
|
|
sessionName: "s",
|
|
execCode: "return 1;",
|
|
validateCode: "return true;",
|
|
},
|
|
{
|
|
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.map((s: { order: number }) => s.order)).toEqual([
|
|
0, 1, 2,
|
|
]);
|
|
});
|
|
|
|
it("preserves id when importing an exported scenario with id", async () => {
|
|
const sc = await createScenario("original");
|
|
const exportRes = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/export`)
|
|
.expect(200);
|
|
|
|
const exported = yamlParse(exportRes.text) as Record<string, unknown>;
|
|
|
|
const importRes = await request(app.getHttpServer())
|
|
.post("/scenarios/import")
|
|
.send(exported)
|
|
.expect(201);
|
|
|
|
expect(importRes.body.id).toBe(sc.id);
|
|
});
|
|
|
|
it("round-trips a scenario faithfully", async () => {
|
|
const sc = await createScenario("roundtrip");
|
|
await createStep(sc.id, {
|
|
order: 0,
|
|
sessionName: "rs",
|
|
execCode: "return 42;",
|
|
validateCode: "return true;",
|
|
});
|
|
|
|
const exportRes = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/export`)
|
|
.expect(200);
|
|
|
|
const exported = yamlParse(exportRes.text) as {
|
|
steps: Array<{ execCode: string; validateCode: string | null }>;
|
|
};
|
|
|
|
const importRes = await request(app.getHttpServer())
|
|
.post("/scenarios/import")
|
|
.send(exported)
|
|
.expect(201);
|
|
|
|
expect(importRes.body.name).toBe("roundtrip");
|
|
expect(importRes.body.steps).toHaveLength(1);
|
|
expect(importRes.body.steps[0].execCode).toBe(
|
|
exported.steps[0].execCode,
|
|
);
|
|
expect(importRes.body.steps[0].validateCode).toBe(
|
|
exported.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("ignores unknown step fields during import", async () => {
|
|
await request(app.getHttpServer())
|
|
.post("/scenarios/import")
|
|
.send({
|
|
name: "bad-type",
|
|
steps: [{ type: "unknown", sessionName: "s" }],
|
|
})
|
|
.expect(201);
|
|
});
|
|
});
|
|
|
|
// ── GET /scenarios/:id/run/:runId ─────────────────────────────────────────
|
|
|
|
describe("GET /scenarios/:id/run/:runId", () => {
|
|
it("returns run with stepRuns (with scenarioStep) and logs array", async () => {
|
|
const sc = await createScenario();
|
|
await createStep(sc.id, { order: 0 });
|
|
const runRes = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run`)
|
|
.expect(201);
|
|
const runId = runRes.body.id;
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/run/${runId}`)
|
|
.expect(200);
|
|
|
|
expect(res.body.id).toBe(runId);
|
|
expect(res.body.status).toBe("pending");
|
|
expect(Array.isArray(res.body.stepRuns)).toBe(true);
|
|
expect(res.body.stepRuns[0]).toHaveProperty("scenarioStep");
|
|
expect(Array.isArray(res.body.logs)).toBe(true);
|
|
});
|
|
|
|
it("stepRuns are ordered by order ASC", async () => {
|
|
const sc = await createScenario();
|
|
await createStep(sc.id, { order: 0 });
|
|
await createStep(sc.id, { order: 1 });
|
|
await createStep(sc.id, { order: 2 });
|
|
const runRes = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run`)
|
|
.expect(201);
|
|
const runId = runRes.body.id;
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/run/${runId}`)
|
|
.expect(200);
|
|
|
|
const orders = res.body.stepRuns.map((s: { order: number }) => s.order);
|
|
expect(orders).toEqual([0, 1, 2]);
|
|
});
|
|
|
|
it("returns 404 for unknown run", async () => {
|
|
const sc = await createScenario();
|
|
await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001`)
|
|
.expect(404);
|
|
});
|
|
|
|
it("returns 404 when run belongs to a different scenario", async () => {
|
|
const sc1 = await createScenario();
|
|
const sc2 = await createScenario();
|
|
await createStep(sc1.id, { order: 0 });
|
|
const runRes = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc1.id}/run`)
|
|
.expect(201);
|
|
const runId = runRes.body.id;
|
|
|
|
await request(app.getHttpServer())
|
|
.get(`/scenarios/${sc2.id}/run/${runId}`)
|
|
.expect(404);
|
|
});
|
|
|
|
it("returns 404 for unknown scenario", async () => {
|
|
await request(app.getHttpServer())
|
|
.get(
|
|
"/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001",
|
|
)
|
|
.expect(404);
|
|
});
|
|
});
|
|
|
|
// ── POST /scenarios/:id/run/:runId/wait ───────────────────────────────────
|
|
|
|
describe("POST /scenarios/:id/run/:runId/wait", () => {
|
|
it("returns 200 with run data immediately when run is already terminal", async () => {
|
|
const sc = await createScenario();
|
|
await createStep(sc.id, { order: 0 });
|
|
const runRes = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run`)
|
|
.expect(201);
|
|
const runId = runRes.body.id;
|
|
|
|
// Manually mark run as pass so wait resolves immediately
|
|
const dataSource = app.get(DataSource);
|
|
await dataSource.query(
|
|
`UPDATE scenario_runs SET status='pass' WHERE id='${runId}'`,
|
|
);
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run/${runId}/wait`)
|
|
.expect(200);
|
|
|
|
expect(res.body.id).toBe(runId);
|
|
expect(res.body.status).toBe("pass");
|
|
expect(Array.isArray(res.body.logs)).toBe(true);
|
|
expect(Array.isArray(res.body.stepRuns)).toBe(true);
|
|
});
|
|
|
|
it("returns the run in fail state when it has failed", async () => {
|
|
const sc = await createScenario();
|
|
await createStep(sc.id, { order: 0 });
|
|
const runRes = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run`)
|
|
.expect(201);
|
|
const runId = runRes.body.id;
|
|
|
|
const dataSource = app.get(DataSource);
|
|
await dataSource.query(
|
|
`UPDATE scenario_runs SET status='fail' WHERE id='${runId}'`,
|
|
);
|
|
|
|
const res = await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run/${runId}/wait`)
|
|
.expect(200);
|
|
|
|
expect(res.body.status).toBe("fail");
|
|
});
|
|
|
|
it("returns 404 for unknown run", async () => {
|
|
const sc = await createScenario();
|
|
await request(app.getHttpServer())
|
|
.post(`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001/wait`)
|
|
.expect(404);
|
|
});
|
|
|
|
it("returns 404 for unknown scenario", async () => {
|
|
await request(app.getHttpServer())
|
|
.post(
|
|
"/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001/wait",
|
|
)
|
|
.expect(404);
|
|
});
|
|
});
|
|
});
|