import { INestApplication } from "@nestjs/common"; import { getRepositoryToken } from "@nestjs/typeorm"; import request from "supertest"; import { buildTestApp } from "./app.harness"; import { SessionEntity } from "../src/session/session.entity"; import { EnvironmentEntity } from "../src/environment/environment.entity"; import { CredentialEntity } from "../src/credential/credential.entity"; import { Repository } from "typeorm"; /** * Browser controller integration tests. * * POST /open and POST /exec need a running Playwright browser. We test * validation rejections and auto-create behavior for unknown named sessions, * which are safe to run in a headless CI environment. */ describe("BrowserController", () => { let app: INestApplication; let sessionRepo: Repository; let environmentRepo: Repository; let credentialRepo: Repository; const FAKE_SESSION = "test-browser-session"; beforeAll(async () => { app = await buildTestApp(); sessionRepo = app.get>( getRepositoryToken(SessionEntity), ); environmentRepo = app.get>( getRepositoryToken(EnvironmentEntity), ); credentialRepo = app.get>( getRepositoryToken(CredentialEntity), ); // Seed a session with minimal but valid JSON so the browser code can // deserialise it (it will still fail to open a real page, tested separately) await sessionRepo.save( sessionRepo.create({ sessionName: FAKE_SESSION, token: "fake-token", cookies: "[]", localStorage: "{}", }), ); }); afterAll(async () => { await app.close(); }); async function createEnvironment( data: Record, name = `browser-env-${Math.random().toString(36).slice(2, 8)}`, ): Promise { const environment = await environmentRepo.save( environmentRepo.create({ name, data, }), ); return environment.id; } async function createCredential( data: Record, name = `browser-cred-${Math.random().toString(36).slice(2, 8)}`, ): Promise { const credential = await credentialRepo.save( credentialRepo.create({ name, data: JSON.stringify(data), lastUsedAt: null, }), ); return credential.id; } // ── POST /open ───────────────────────────────────────────────────────────── describe("POST /open", () => { it("returns 400 when body is empty", async () => { await request(app.getHttpServer()).post("/open").send({}).expect(400); }); it("returns 400 when url is missing", async () => { await request(app.getHttpServer()) .post("/open") .send({ sessionName: FAKE_SESSION }) .expect(400); }); it("auto-creates missing named session and succeeds", async () => { const res = await request(app.getHttpServer()) .post("/open") .send({ sessionName: "no-such-session", url: "https://example.com" }) .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 () => { const res = await request(app.getHttpServer()) .post("/open") .send({ url: "https://example.com" }) .expect(201); expect(res.body).toMatchObject({ url: expect.any(String), title: expect.any(String), content: expect.any(String), }); }); it("returns only selector content when selector is provided", async () => { const res = await request(app.getHttpServer()) .post("/open") .send({ url: "https://example.com", selector: "#mock" }) .expect(201); expect(res.body.content).toBe('
mock content
'); }); it("returns selector text content in reader mode", async () => { const res = await request(app.getHttpServer()) .post("/open") .send({ url: "https://example.com", selector: "#mock", readerMode: true, }) .expect(201); expect(res.body.content).toBe("mock content"); }); }); // ── POST /exec ───────────────────────────────────────────────────────────── describe("POST /exec", () => { it("returns 400 when body is empty", async () => { await request(app.getHttpServer()).post("/exec").send({}).expect(400); }); it("returns 400 when code is missing", async () => { await request(app.getHttpServer()) .post("/exec") .send({ sessionName: FAKE_SESSION }) .expect(400); }); it("returns 400 when code has a syntax error", async () => { await request(app.getHttpServer()) .post("/exec") .send({ sessionName: FAKE_SESSION, code: "this is not valid {{{" }) .expect(400); }); it("auto-creates missing named session and succeeds", async () => { const res = await request(app.getHttpServer()) .post("/exec") .send({ sessionName: "no-such-session", code: "return 1;" }) .expect(201); expect(res.body).toEqual({ result: 1 }); }); it("succeeds without a session (sessionless exec)", async () => { const res = await request(app.getHttpServer()) .post("/exec") .send({ code: "return 42;" }) .expect(201); expect(res.body).toEqual({ result: 42 }); }); it("exposes environment via context.env in a named session", async () => { const environmentId = await createEnvironment({ BASE_URL: "https://env.example.com", }); const res = await request(app.getHttpServer()) .post("/exec") .send({ sessionName: "no-such-session", code: "return context.env.BASE_URL;", environmentId, }) .expect(201); expect(res.body).toEqual({ result: "https://env.example.com" }); }); it("exposes environment via context.env in a sessionless exec", async () => { const environmentId = await createEnvironment({ KEY: "value123" }); const res = await request(app.getHttpServer()) .post("/exec") .send({ code: "return context.env.KEY;", environmentId, }) .expect(201); expect(res.body).toEqual({ result: "value123" }); }); it("exposes credentials via context.getCredential in a named session", async () => { const credentialId = await createCredential({ username: "user1", password: "pass1", }); const res = await request(app.getHttpServer()) .post("/exec") .send({ sessionName: "no-such-session", code: "return context.getCredential('admin');", credentials: { admin: credentialId }, }) .expect(201); expect(res.body).toEqual({ result: { username: "user1", password: "pass1" }, }); }); it("exposes credentials via context.getCredential in a sessionless exec", async () => { const credentialId = await createCredential({ token: "abc" }); const res = await request(app.getHttpServer()) .post("/exec") .send({ code: "return context.getCredential('svc');", credentials: { svc: credentialId }, }) .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 context.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(context.env).length === 0 ? 'empty' : 'not-empty';", }) .expect(201); expect(res.body).toEqual({ result: "empty" }); }); }); });