feat(browser): add environment, credentials, and script logger to POST /exec
- ExecDto gains optional `environment` and `credentials` fields - BrowserService.exec forwards both to CodeExecutorService.execute so helpers.env, helpers.getEnvUrl(), and helpers.getCredential() work identically to scenario-scheduler steps - script console.log/warn/error now routed through TraceLogger with session label prefix - integration tests cover env/creds injection and missing-alias error
This commit is contained in:
@@ -56,6 +56,6 @@ export class BrowserController {
|
|||||||
@ApiResponse({ status: 500, description: "Execution failed" })
|
@ApiResponse({ status: 500, description: "Execution failed" })
|
||||||
exec(@Body() dto: ExecDto): Promise<ExecResult> {
|
exec(@Body() dto: ExecDto): Promise<ExecResult> {
|
||||||
this.codeExecutor.validate(dto.code);
|
this.codeExecutor.validate(dto.code);
|
||||||
return this.browserService.exec(dto.sessionName, dto.code, dto.url);
|
return this.browserService.exec(dto.sessionName, dto.code, dto.url, dto.environment, dto.credentials);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import { JSDOM } from "jsdom";
|
|||||||
import { SessionContextService } from "../session/session-context.service";
|
import { SessionContextService } from "../session/session-context.service";
|
||||||
import { SessionService } from "../session/session.service";
|
import { SessionService } from "../session/session.service";
|
||||||
import { CodeExecutorService } from "../code-executor/code-executor.service";
|
import { CodeExecutorService } from "../code-executor/code-executor.service";
|
||||||
import type { ExecResult } from "../code-executor/code-executor.service";
|
import type { ExecResult, ScriptLogger } from "../code-executor/code-executor.service";
|
||||||
import { SnippetService } from "../snippet/snippet.service";
|
import { SnippetService } from "../snippet/snippet.service";
|
||||||
import type { Cookie } from "playwright";
|
import type { Cookie } from "playwright";
|
||||||
|
import type { EnvironmentData } from "../environment/environment.entity";
|
||||||
|
|
||||||
export type { ExecResult } from "../code-executor/code-executor.service";
|
export type { ExecResult } from "../code-executor/code-executor.service";
|
||||||
|
|
||||||
@@ -166,6 +167,8 @@ export class BrowserService {
|
|||||||
sessionName: string | undefined,
|
sessionName: string | undefined,
|
||||||
code: string,
|
code: string,
|
||||||
url?: string,
|
url?: string,
|
||||||
|
environment?: EnvironmentData,
|
||||||
|
credentials?: Record<string, unknown>,
|
||||||
): Promise<ExecResult> {
|
): Promise<ExecResult> {
|
||||||
const label = sessionName ?? "anonymous";
|
const label = sessionName ?? "anonymous";
|
||||||
|
|
||||||
@@ -182,14 +185,16 @@ export class BrowserService {
|
|||||||
await page.goto(url, { waitUntil: "networkidle" });
|
await page.goto(url, { waitUntil: "networkidle" });
|
||||||
}
|
}
|
||||||
this.logger.log(`[${label}] exec: running user code`);
|
this.logger.log(`[${label}] exec: running user code`);
|
||||||
|
const scriptLogger: ScriptLogger = (level, msg) =>
|
||||||
|
this.logger[level](`[${label}] script: ${msg}`);
|
||||||
const result = await this.codeExecutor.execute(
|
const result = await this.codeExecutor.execute(
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
code,
|
code,
|
||||||
|
scriptLogger,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
credentials,
|
||||||
undefined,
|
environment,
|
||||||
undefined,
|
|
||||||
snippets,
|
snippets,
|
||||||
);
|
);
|
||||||
this.logger.log(`[${label}] exec: done`);
|
this.logger.log(`[${label}] exec: done`);
|
||||||
@@ -217,14 +222,16 @@ export class BrowserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[${label}] exec: running user code`);
|
this.logger.log(`[${label}] exec: running user code`);
|
||||||
|
const scriptLogger: ScriptLogger = (level, msg) =>
|
||||||
|
this.logger[level](`[${label}] script: ${msg}`);
|
||||||
const result = await this.codeExecutor.execute(
|
const result = await this.codeExecutor.execute(
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
code,
|
code,
|
||||||
|
scriptLogger,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
credentials,
|
||||||
undefined,
|
environment,
|
||||||
undefined,
|
|
||||||
snippetMap,
|
snippetMap,
|
||||||
);
|
);
|
||||||
this.logger.log(`[${label}] exec: done`);
|
this.logger.log(`[${label}] exec: done`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsOptional, IsString, IsUrl } from "class-validator";
|
import { IsObject, IsOptional, IsString, IsUrl } from "class-validator";
|
||||||
|
import type { EnvironmentData } from "../../environment/environment.entity";
|
||||||
|
|
||||||
export class ExecDto {
|
export class ExecDto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@@ -27,4 +28,22 @@ export class ExecDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Key/value map of environment variables available via `helpers.env` and `helpers.getEnvUrl()` in the script.",
|
||||||
|
example: { BASE_URL: "https://example.com" },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
environment?: EnvironmentData;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Key/value map of credentials available via `helpers.getCredential()` in the script.",
|
||||||
|
example: { admin: { username: "user", password: "pass" } },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
credentials?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,5 +135,74 @@ describe("BrowserController", () => {
|
|||||||
.expect(201);
|
.expect(201);
|
||||||
expect(res.body).toEqual({ result: 42 });
|
expect(res.body).toEqual({ result: 42 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes environment via helpers.env in a named session", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
sessionName: "no-such-session",
|
||||||
|
code: "return helpers.env.BASE_URL;",
|
||||||
|
environment: { BASE_URL: "https://env.example.com" },
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
expect(res.body).toEqual({ result: "https://env.example.com" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes environment via helpers.env in a sessionless exec", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
code: "return helpers.env.KEY;",
|
||||||
|
environment: { KEY: "value123" },
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
expect(res.body).toEqual({ result: "value123" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes credentials via helpers.getCredential in a named session", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
sessionName: "no-such-session",
|
||||||
|
code: "return helpers.getCredential('admin');",
|
||||||
|
credentials: { admin: { username: "user1", password: "pass1" } },
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
result: { username: "user1", password: "pass1" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes credentials via helpers.getCredential in a sessionless exec", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
code: "return helpers.getCredential('svc');",
|
||||||
|
credentials: { svc: { token: "abc" } },
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
expect(res.body).toEqual({ result: { token: "abc" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when getCredential is called with an unknown alias", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
code: "return helpers.getCredential('missing');",
|
||||||
|
credentials: {},
|
||||||
|
})
|
||||||
|
.expect(500);
|
||||||
|
expect(res.body.message).toMatch(/missing/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("environment and credentials default to empty when omitted", async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post("/exec")
|
||||||
|
.send({
|
||||||
|
code: "return Object.keys(helpers.env).length === 0 ? 'empty' : 'not-empty';",
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
expect(res.body).toEqual({ result: "empty" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user