import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { BrowserService, BrowserServiceError, ENV_PROVIDER_NAME } from "../../src/modules/browser/service.ts"; import type { ServerContext } from "../../src/modules/types.ts"; import { ACCESS_KEY, type BrowserHarness, createBrowserHarness, type Loose, pgAvailable } from "./harness.ts"; // BR-1:浏览器服务登记 —— 环境变量(BROWSER_PROVIDER_URL / BROWSER_ACCESS_KEY / BROWSER_ENABLED → camofox-env) // 与设置页端点(POST / PATCH /browser/providers、POST /browser/providers/{id}/probe)。 const up = await pgAvailable(); describe.skipIf(!up)("浏览器服务登记", () => { let h: BrowserHarness; const withEnv = (env: Record) => new BrowserService({ ...h.ctx, config: { ...(h.ctx.config as object), ...env } } as ServerContext, {}); beforeAll(async () => { h = await createBrowserHarness("provcfg"); }, 120_000); afterAll(async () => { await h?.close(); }); it("BROWSER_PROVIDER_URL:登记 camofox-env,凭据只在进程内;探测带 Bearer;地址/凭据不能在设置页改", async () => { const svc = withEnv({ BROWSER_PROVIDER_URL: h.mock.url, BROWSER_ACCESS_KEY: ACCESS_KEY, BROWSER_ENABLED: true }); const list = await svc.listProviders(true); const env = list.find((p) => p.name === ENV_PROVIDER_NAME)!; expect(env).toMatchObject({ managedBy: "env", hasAccessKey: true, secretRef: null, enabled: true }); expect(env.health?.status).toBe("ok"); const row = await h.db .selectFrom("browser_provider") .selectAll() .where("name", "=", ENV_PROVIDER_NAME) .executeTakeFirstOrThrow(); expect(JSON.stringify(row)).not.toContain(ACCESS_KEY); expect(h.mock.calls.filter((c) => c.path === "/openapi.json").at(-1)?.headers.authorization).toBe( `Bearer ${ACCESS_KEY}`, ); await expect(svc.updateProvider(env.id, { baseUrl: "http://x:1" })).rejects.toBeInstanceOf(BrowserServiceError); // 启用可以改 expect((await svc.updateProvider(env.id, { enabled: false })).enabled).toBe(false); }); it("BROWSER_ENABLED=false / 去掉地址:camofox-env 停用,其它服务不受影响", async () => { await withEnv({ BROWSER_PROVIDER_URL: h.mock.url, BROWSER_ENABLED: false }).syncEnvProvider(); const off = await h.db .selectFrom("browser_provider") .selectAll() .where("name", "=", ENV_PROVIDER_NAME) .executeTakeFirstOrThrow(); expect(off.enabled).toBe(false); await withEnv({ BROWSER_PROVIDER_URL: h.mock.url, BROWSER_ENABLED: true }).syncEnvProvider(); await withEnv({}).syncEnvProvider(); const rows = await h.db.selectFrom("browser_provider").select(["name", "enabled"]).orderBy("name").execute(); expect(rows).toEqual([ { name: "camofox", enabled: true }, { name: ENV_PROVIDER_NAME, enabled: false }, ]); }); it("设置页:登记 / 改凭据 / 测试连通;凭据明文不出 API,名称 camofox-env 保留", async () => { const bad = await h.req("/browser/providers", { body: { name: ENV_PROVIDER_NAME, baseUrl: h.mock.url } }); expect(bad.status).toBe(400); const c = await h.req("/browser/providers", { body: { name: "my-camofox", baseUrl: h.mock.url, accessKey: "wrong-key", enabled: true }, }); expect(c.status).toBe(200); const id = c.body.data.id as string; expect(c.body.data).toMatchObject({ managedBy: null, hasAccessKey: true, enabled: true }); expect(JSON.stringify(c.body)).not.toContain("wrong-key"); const dup = await h.req("/browser/providers", { body: { name: "my-camofox", baseUrl: h.mock.url } }); expect(dup.status).toBe(409); // 错误凭据:/health 免认证是好的,但 /openapi.json 401 → 标离线并注明 auth const p1 = await h.req(`/browser/providers/${id}/probe`, { body: {} }); expect(p1.body.data).toMatchObject({ healthStatus: "offline", health: { status: "down", detail: { error: "auth" } }, }); const u = await h.req(`/browser/providers/${id}`, { method: "PATCH", body: { accessKey: ACCESS_KEY } }); expect(u.status).toBe(200); const p2 = await h.req(`/browser/providers/${id}/probe`, { body: {} }); expect(p2.status).toBe(200); expect((p2.body.data as Loose).capabilities.upstreamVersion).toBe("1.17.0-mock"); // 空串 = 保持原凭据(FIX-8 只写凭证统一语义) const kept = await h.req(`/browser/providers/${id}`, { method: "PATCH", body: { accessKey: "", enabled: true } }); expect(kept.body.data).toMatchObject({ hasAccessKey: true, secretRef: u.body.data.secretRef }); const cleared = await h.req(`/browser/providers/${id}`, { method: "PATCH", body: { accessKey: null } }); expect(cleared.body.data).toMatchObject({ hasAccessKey: false, secretRef: null }); expect((await h.req(`/browser/providers/${id}`, { method: "DELETE" })).status).toBe(200); const env = (await h.req("/browser/providers")).body.data.items.find((p: Loose) => p.name === ENV_PROVIDER_NAME); expect((await h.req(`/browser/providers/${env.id}`, { method: "DELETE" })).status).toBe(409); }); });