From 2401dbd68fbf629a6286146575447f3c504040f6 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 15:59:49 +0200 Subject: [PATCH] test(ai): settings page never exposes the API key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tests cover the storage and access boundary: - A client manager cannot list the settings collection at all (rules are superuser-only — verified by the rejection, not by silent skip). - The settings page HTML returned to the owner does not contain the key value, even when the key is freshly set to a unique sentinel. - A client manager gets 404 on /admin/settings (admin layout gates on role, not on session presence). helpers.ts gained ownerEmail / ownerPassword — needed by the settings test and reusable for any owner-only integration test going forward. --- tests/integration/ai-settings.test.ts | 48 +++++++++++++++++++++++++++ tests/integration/helpers.ts | 2 ++ 2 files changed, 50 insertions(+) create mode 100644 tests/integration/ai-settings.test.ts diff --git a/tests/integration/ai-settings.test.ts b/tests/integration/ai-settings.test.ts new file mode 100644 index 0000000..e574df0 --- /dev/null +++ b/tests/integration/ai-settings.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import PocketBase from "pocketbase"; + +import { APP_URL, DEMO, adminPb, requireServices, signIn, userPb } from "./helpers"; + +let admin: PocketBase; + +beforeAll(async () => { + await requireServices(); + admin = await adminPb(); +}); + +describe("the AI key never leaves the server", () => { + it("is not readable by a client manager", async () => { + const pb = await userPb(DEMO.managerEmail, DEMO.managerPassword); + // Every rule on `settings` is null — superuser only. + await expect(pb.collection("settings").getFullList()).rejects.toThrow(); + }); + + it("does not appear in the settings page HTML", async () => { + const marker = `sk-test-should-not-leak-${Date.now()}`; + + const existing = await admin + .collection("settings") + .getFirstListItem("id != ''") + .catch(() => null); + const record = existing + ? await admin.collection("settings").update(existing.id, { ai_api_key: marker }) + : await admin.collection("settings").create({ ai_api_key: marker }); + + const cookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword); + const response = await fetch(`${APP_URL}/admin/settings`, { headers: { cookie } }); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(html).not.toContain(marker); + + await admin.collection("settings").update(record.id, { ai_api_key: "" }); + }); +}); + +describe("the admin settings page is owner-only", () => { + it("returns 404 for a client manager", async () => { + const cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword); + const response = await fetch(`${APP_URL}/admin/settings`, { headers: { cookie } }); + expect(response.status).toBe(404); + }); +}); \ No newline at end of file diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 292b3d0..283bb10 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -8,6 +8,8 @@ export const DEMO = { projectSlug: "wfdash-webapp", managerEmail: "manager@wf.example.com", managerPassword: "manager-password-123", + ownerEmail: "owner@example.com", + ownerPassword: "owner-password-123", }; /**