test(server): align specs with run environment and step schema

- update scenario tests to create runs with required environmentId
- adjust browser controller expectations to session auto-create behavior
- sync playwright/context mocks with current browser service usage
This commit is contained in:
2026-04-11 00:22:58 +03:00
parent 56d23f913d
commit 29e91b0078
4 changed files with 69 additions and 49 deletions
+1
View File
@@ -9,6 +9,7 @@ const makePage = () => ({
const makeContext = () => ({ const makeContext = () => ({
newPage: jest.fn().mockResolvedValue(makePage()), newPage: jest.fn().mockResolvedValue(makePage()),
cookies: jest.fn().mockResolvedValue([]),
addCookies: jest.fn().mockResolvedValue(undefined), addCookies: jest.fn().mockResolvedValue(undefined),
addInitScript: jest.fn().mockResolvedValue(undefined), addInitScript: jest.fn().mockResolvedValue(undefined),
}); });
+14 -8
View File
@@ -8,8 +8,8 @@ import { Repository } from "typeorm";
/** /**
* Browser controller integration tests. * Browser controller integration tests.
* *
* POST /open and POST /exec need a running Playwright browser. We test * POST /open and POST /exec need a running Playwright browser. We test
* validation rejections (no browser launched) and session-not-found paths, * validation rejections and auto-create behavior for unknown named sessions,
* which are safe to run in a headless CI environment. * which are safe to run in a headless CI environment.
*/ */
describe("BrowserController", () => { describe("BrowserController", () => {
@@ -54,11 +54,16 @@ describe("BrowserController", () => {
.expect(400); .expect(400);
}); });
it("returns 404 when session does not exist", async () => { it("auto-creates missing named session and succeeds", async () => {
await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post("/open") .post("/open")
.send({ sessionName: "no-such-session", url: "https://example.com" }) .send({ sessionName: "no-such-session", url: "https://example.com" })
.expect(404); .expect(201);
expect(res.body).toMatchObject({
url: expect.any(String),
title: expect.any(String),
content: expect.any(String),
});
}); });
it("succeeds without a session (sessionless open)", async () => { it("succeeds without a session (sessionless open)", async () => {
@@ -115,11 +120,12 @@ describe("BrowserController", () => {
.expect(400); .expect(400);
}); });
it("returns 404 when session does not exist", async () => { it("auto-creates missing named session and succeeds", async () => {
await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post("/exec") .post("/exec")
.send({ sessionName: "no-such-session", code: "return 1;" }) .send({ sessionName: "no-such-session", code: "return 1;" })
.expect(404); .expect(201);
expect(res.body).toEqual({ result: 1 });
}); });
it("succeeds without a session (sessionless exec)", async () => { it("succeeds without a session (sessionless exec)", async () => {
+12 -3
View File
@@ -15,6 +15,7 @@ import { DataSource } from "typeorm";
import { buildTestApp } from "./app.harness"; import { buildTestApp } from "./app.harness";
import { ScenarioService } from "../src/scenario/scenario.service"; import { ScenarioService } from "../src/scenario/scenario.service";
import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service"; import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service";
import { EnvironmentEntity } from "../src/environment/environment.entity";
describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
let app: INestApplication; let app: INestApplication;
@@ -52,18 +53,26 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
scenarioId: string, scenarioId: string,
_order: number, _order: number,
execCode: string, execCode: string,
sessionName = "output-test-session",
): Promise<string> { ): Promise<string> {
const step = await scenarioService.createStep(scenarioId, { const step = await scenarioService.createStep(scenarioId, {
sessionName,
execCode, execCode,
}); });
return step.id; return step.id;
} }
/** Create a test environment and return its id. */
async function createEnvironment(
name = `output-env-${Math.random().toString(36).slice(2, 8)}`,
): Promise<string> {
const repo = dataSource.getRepository(EnvironmentEntity);
const env = await repo.save(repo.create({ name, data: {} }));
return env.id;
}
/** Trigger a run and process it to completion via the scheduler. */ /** Trigger a run and process it to completion via the scheduler. */
async function runScenario(scenarioId: string): Promise<string> { async function runScenario(scenarioId: string): Promise<string> {
const run = await scenarioService.createRun(scenarioId); const environmentId = await createEnvironment();
const run = await scenarioService.createRun(scenarioId, environmentId);
// Drive the scheduler directly — keeps tests synchronous and fast. // Drive the scheduler directly — keeps tests synchronous and fast.
await scheduler.pickUpPendingRuns(); await scheduler.pickUpPendingRuns();
// Wait for the run to reach a terminal state (max 10 s). // Wait for the run to reach a terminal state (max 10 s).
+42 -38
View File
@@ -41,6 +41,27 @@ describe("ScenarioController", () => {
return res.body as { id: string }; return res.body as { id: string };
} }
async function createEnvironment(name = `env-${Math.random()}`) {
const res = await request(app.getHttpServer())
.post("/environments")
.send({ name, data: {} })
.expect(201);
return res.body as { id: string; name: string };
}
async function createRun(scenarioId: string) {
const env = await createEnvironment();
const res = await request(app.getHttpServer())
.post(`/scenarios/${scenarioId}/run`)
.send({ environmentId: env.id })
.expect(201);
return res.body as {
id: string;
status: string;
stepRuns: Array<{ status: string }>;
};
}
// ── POST /scenarios ──────────────────────────────────────────────────────── // ── POST /scenarios ────────────────────────────────────────────────────────
describe("POST /scenarios", () => { describe("POST /scenarios", () => {
@@ -190,25 +211,22 @@ describe("ScenarioController", () => {
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`) .post(`/scenarios/${sc.id}/steps`)
.send({ .send({
order: 0,
sessionName: "my-session",
execCode: "return 1;", execCode: "return 1;",
}) })
.expect(201); .expect(201);
expect(res.body.id).toBeDefined(); expect(res.body.id).toBeDefined();
expect(res.body.order).toBe(0); expect(res.body.order).toBe(0);
expect(res.body.sessionName).toBe("my-session");
}); });
it("creates a step without execCode", async () => { it("creates a step without execCode", async () => {
const sc = await createScenario(); const sc = await createScenario();
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`) .post(`/scenarios/${sc.id}/steps`)
.send({ order: 0, sessionName: "session-x" }) .send({ title: "step without exec" })
.expect(201); .expect(201);
expect(res.body.sessionName).toBe("session-x"); expect(res.body.title).toBe("step without exec");
expect(res.body.execCode).toBeNull(); expect(res.body.execCode).toBeNull();
}); });
@@ -371,15 +389,13 @@ describe("ScenarioController", () => {
await createStep(sc.id, { order: 1 }); await createStep(sc.id, { order: 1 });
await createStep(sc.id, { order: 2 }); await createStep(sc.id, { order: 2 });
const res = await request(app.getHttpServer()) const res = await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`)
.expect(201);
expect(res.body.status).toBe("pending"); expect(res.status).toBe("pending");
expect(Array.isArray(res.body.stepRuns)).toBe(true); expect(Array.isArray(res.stepRuns)).toBe(true);
expect(res.body.stepRuns).toHaveLength(3); expect(res.stepRuns).toHaveLength(3);
const statuses = res.body.stepRuns.map( const statuses = res.stepRuns.map(
(s: { status: string }) => s.status, (s: { status: string }) => s.status,
); );
expect(statuses[0]).toBe("pending"); expect(statuses[0]).toBe("pending");
@@ -388,8 +404,10 @@ describe("ScenarioController", () => {
}); });
it("returns 404 for unknown scenario", async () => { it("returns 404 for unknown scenario", async () => {
const env = await createEnvironment();
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/scenarios/00000000-0000-0000-0000-000000000001/run") .post("/scenarios/00000000-0000-0000-0000-000000000001/run")
.send({ environmentId: env.id })
.expect(404); .expect(404);
}); });
}); });
@@ -400,9 +418,7 @@ describe("ScenarioController", () => {
it("returns paginated runs with stepRuns embedded", async () => { it("returns paginated runs with stepRuns embedded", async () => {
const sc = await createScenario(); const sc = await createScenario();
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
await request(app.getHttpServer()) await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`)
.expect(201);
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs`) .get(`/scenarios/${sc.id}/runs`)
@@ -415,9 +431,7 @@ describe("ScenarioController", () => {
it("filters by status", async () => { it("filters by status", async () => {
const sc = await createScenario(); const sc = await createScenario();
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
await request(app.getHttpServer()) await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`)
.expect(201);
const pendingRes = await request(app.getHttpServer()) const pendingRes = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs?status=pending`) .get(`/scenarios/${sc.id}/runs?status=pending`)
@@ -675,10 +689,8 @@ describe("ScenarioController", () => {
it("returns run with stepRuns (with scenarioStep) and logs array", async () => { it("returns run with stepRuns (with scenarioStep) and logs array", async () => {
const sc = await createScenario(); const sc = await createScenario();
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
const runRes = await request(app.getHttpServer()) const runRes = await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`) const runId = runRes.id;
.expect(201);
const runId = runRes.body.id;
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/run/${runId}`) .get(`/scenarios/${sc.id}/run/${runId}`)
@@ -696,10 +708,8 @@ describe("ScenarioController", () => {
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
await createStep(sc.id, { order: 1 }); await createStep(sc.id, { order: 1 });
await createStep(sc.id, { order: 2 }); await createStep(sc.id, { order: 2 });
const runRes = await request(app.getHttpServer()) const runRes = await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`) const runId = runRes.id;
.expect(201);
const runId = runRes.body.id;
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/run/${runId}`) .get(`/scenarios/${sc.id}/run/${runId}`)
@@ -720,10 +730,8 @@ describe("ScenarioController", () => {
const sc1 = await createScenario(); const sc1 = await createScenario();
const sc2 = await createScenario(); const sc2 = await createScenario();
await createStep(sc1.id, { order: 0 }); await createStep(sc1.id, { order: 0 });
const runRes = await request(app.getHttpServer()) const runRes = await createRun(sc1.id);
.post(`/scenarios/${sc1.id}/run`) const runId = runRes.id;
.expect(201);
const runId = runRes.body.id;
await request(app.getHttpServer()) await request(app.getHttpServer())
.get(`/scenarios/${sc2.id}/run/${runId}`) .get(`/scenarios/${sc2.id}/run/${runId}`)
@@ -745,10 +753,8 @@ describe("ScenarioController", () => {
it("returns 200 with run data immediately when run is already terminal", async () => { it("returns 200 with run data immediately when run is already terminal", async () => {
const sc = await createScenario(); const sc = await createScenario();
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
const runRes = await request(app.getHttpServer()) const runRes = await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`) const runId = runRes.id;
.expect(201);
const runId = runRes.body.id;
// Manually mark run as pass so wait resolves immediately // Manually mark run as pass so wait resolves immediately
const dataSource = app.get(DataSource); const dataSource = app.get(DataSource);
@@ -769,10 +775,8 @@ describe("ScenarioController", () => {
it("returns the run in fail state when it has failed", async () => { it("returns the run in fail state when it has failed", async () => {
const sc = await createScenario(); const sc = await createScenario();
await createStep(sc.id, { order: 0 }); await createStep(sc.id, { order: 0 });
const runRes = await request(app.getHttpServer()) const runRes = await createRun(sc.id);
.post(`/scenarios/${sc.id}/run`) const runId = runRes.id;
.expect(201);
const runId = runRes.body.id;
const dataSource = app.get(DataSource); const dataSource = app.get(DataSource);
await dataSource.query( await dataSource.query(