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:
2026-04-14 16:57:06 +03:00
parent d34ea9e943
commit 1a2e786ca8
4 changed files with 104 additions and 9 deletions
+1 -1
View File
@@ -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);
} }
} }
+14 -7
View File
@@ -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`);
+20 -1
View File
@@ -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>;
} }
+69
View File
@@ -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" });
});
}); });
}); });