From 1a2e786ca8e5b45008cf8f82618e7f43496cd882 Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 14 Apr 2026 16:57:06 +0300 Subject: [PATCH] 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 --- server/src/browser/browser.controller.ts | 2 +- server/src/browser/browser.service.ts | 21 +++++--- server/src/browser/dto/exec.dto.ts | 21 +++++++- server/test/browser.controller.spec.ts | 69 ++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 9 deletions(-) diff --git a/server/src/browser/browser.controller.ts b/server/src/browser/browser.controller.ts index 7a48d14..dff50d2 100644 --- a/server/src/browser/browser.controller.ts +++ b/server/src/browser/browser.controller.ts @@ -56,6 +56,6 @@ export class BrowserController { @ApiResponse({ status: 500, description: "Execution failed" }) exec(@Body() dto: ExecDto): Promise { 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); } } diff --git a/server/src/browser/browser.service.ts b/server/src/browser/browser.service.ts index abd84d6..122af59 100644 --- a/server/src/browser/browser.service.ts +++ b/server/src/browser/browser.service.ts @@ -11,9 +11,10 @@ import { JSDOM } from "jsdom"; import { SessionContextService } from "../session/session-context.service"; import { SessionService } from "../session/session.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 type { Cookie } from "playwright"; +import type { EnvironmentData } from "../environment/environment.entity"; export type { ExecResult } from "../code-executor/code-executor.service"; @@ -166,6 +167,8 @@ export class BrowserService { sessionName: string | undefined, code: string, url?: string, + environment?: EnvironmentData, + credentials?: Record, ): Promise { const label = sessionName ?? "anonymous"; @@ -182,14 +185,16 @@ export class BrowserService { await page.goto(url, { waitUntil: "networkidle" }); } 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( page, context, code, + scriptLogger, undefined, - undefined, - undefined, - undefined, + credentials, + environment, snippets, ); this.logger.log(`[${label}] exec: done`); @@ -217,14 +222,16 @@ export class BrowserService { } 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( page, context, code, + scriptLogger, undefined, - undefined, - undefined, - undefined, + credentials, + environment, snippetMap, ); this.logger.log(`[${label}] exec: done`); diff --git a/server/src/browser/dto/exec.dto.ts b/server/src/browser/dto/exec.dto.ts index a1968e8..f9753d3 100644 --- a/server/src/browser/dto/exec.dto.ts +++ b/server/src/browser/dto/exec.dto.ts @@ -1,5 +1,6 @@ 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 { @ApiPropertyOptional({ @@ -27,4 +28,22 @@ export class ExecDto { }) @IsString() 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; } diff --git a/server/test/browser.controller.spec.ts b/server/test/browser.controller.spec.ts index 3e32fac..ee73deb 100644 --- a/server/test/browser.controller.spec.ts +++ b/server/test/browser.controller.spec.ts @@ -135,5 +135,74 @@ describe("BrowserController", () => { .expect(201); 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" }); + }); }); });