import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { DEV_KEY, json } from "../helpers.ts"; import { bearer, createHarness, type Harness, pgAvailable } from "./harness.ts"; // FIX-8 只写凭证统一语义:省略或空串 = 保持原值,null = 清除,非空 = 替换;明文不出 API。 const hasPg = await pgAvailable(); describe.skipIf(!hasPg)("只写凭证:留空不变 / 显式清除(真实 PG)", () => { let h: Harness; let admin: ReturnType; beforeAll(async () => { h = await createHarness({ engine: false }); admin = bearer(h.app, DEV_KEY); }, 60_000); afterAll(async () => { await h?.close(); }); const secretOf = (ref: string) => h.ctx.secrets.resolve(ref); it("下载器(账密):空串保持;null 清除;新值原地替换", async () => { const c = ( await json( await admin("POST", "/downloader-instances", { name: "qb-keep", baseUrl: "http://127.0.0.1:1", authKind: "password", username: "u", credential: "pw-original", }), ) ).data; const ref = c.secretRef as string; expect(ref).toBeTruthy(); const kept = (await json(await admin("PATCH", `/downloader-instances/${c.id}`, { credential: "", name: "qb-k2" }))) .data; expect(kept.secretRef).toBe(ref); expect(await secretOf(ref)).toBe("pw-original"); const ws = (await json(await admin("PATCH", `/downloader-instances/${c.id}`, { credential: " " }))).data; expect(ws.secretRef).toBe(ref); const replaced = (await json(await admin("PATCH", `/downloader-instances/${c.id}`, { credential: "pw-new" }))).data; expect(replaced.secretRef).toBe(ref); expect(await secretOf(ref)).toBe("pw-new"); const cleared = (await json(await admin("PATCH", `/downloader-instances/${c.id}`, { credential: null }))).data; expect(cleared.secretRef).toBeNull(); await expect(secretOf(ref)).rejects.toThrow(/不存在/); expect(JSON.stringify(cleared)).not.toContain("pw-"); }); it("下载器(API Key):不能清除 Key(422),留空保持", async () => { const c = ( await json( await admin("POST", "/downloader-instances", { name: "qb-key", baseUrl: "http://127.0.0.1:1", authKind: "api_key", credential: "qbt_key_1", }), ) ).data; expect((await admin("PATCH", `/downloader-instances/${c.id}`, { credential: null })).status).toBe(422); const kept = (await json(await admin("PATCH", `/downloader-instances/${c.id}`, { credential: "" }))).data; expect(kept.secretRef).toBe(c.secretRef); // 新建时空串 = 没填:API Key 认证必须有 Key expect( ( await admin("POST", "/downloader-instances", { name: "qb-empty", baseUrl: "http://127.0.0.1:1", authKind: "api_key", credential: "", }) ).status, ).toBe(422); }); it("媒体服务器:空串保持;null 清除", async () => { const c = ( await json( await admin("POST", "/media-server-instances", { name: "emby-keep", baseUrl: "http://127.0.0.1:1", credential: "emby-key-1", }), ) ).data; const ref = c.secretRef as string; const kept = (await json(await admin("PATCH", `/media-server-instances/${c.id}`, { credential: "" }))).data; expect(kept.secretRef).toBe(ref); expect(await secretOf(ref)).toBe("emby-key-1"); const cleared = (await json(await admin("PATCH", `/media-server-instances/${c.id}`, { credential: null }))).data; expect(cleared.secretRef).toBeNull(); await expect(secretOf(ref)).rejects.toThrow(/不存在/); // 空串新建 = 不存 key const n = ( await json( await admin("POST", "/media-server-instances", { name: "emby-nokey", baseUrl: "http://127.0.0.1:1", credential: "", }), ) ).data; expect(n.secretRef).toBeNull(); }); it("设置(TMDB / 网页搜索 Key):空串保持;null 清除;审计不留明文", async () => { const set = (await json(await admin("PATCH", "/settings", { metadata: { tmdbApiKey: "tmdb-secret-1" } }))).data; const ref = set.metadata.tmdbApiKeySecretRef as string; expect(ref).toBeTruthy(); const kept = ( await json(await admin("PATCH", "/settings", { metadata: { tmdbApiKey: "", tmdbBaseUrl: "https://t.local" } })) ).data; expect(kept.metadata.tmdbApiKeySecretRef).toBe(ref); expect(kept.metadata.tmdbBaseUrl).toBe("https://t.local"); expect(await secretOf(ref)).toBe("tmdb-secret-1"); const cleared = (await json(await admin("PATCH", "/settings", { metadata: { tmdbApiKey: null } }))).data; expect(cleared.metadata.tmdbApiKeySecretRef).toBeNull(); const ws = ( await json(await admin("PATCH", "/settings", { agent: { webSearch: { provider: "brave", apiKey: "brave-1" } } })) ).data; const wref = ws.agent.webSearch.apiKeySecretRef as string; const wkept = (await json(await admin("PATCH", "/settings", { agent: { webSearch: { apiKey: "" } } }))).data; expect(wkept.agent.webSearch.apiKeySecretRef).toBe(wref); const wcleared = (await json(await admin("PATCH", "/settings", { agent: { webSearch: { apiKey: null } } }))).data; expect(wcleared.agent.webSearch.apiKeySecretRef).toBeNull(); const audits = await h.db .selectFrom("audit_event") .select("payload") .where("action", "=", "settings.update") .execute(); const text = JSON.stringify(audits); expect(text).not.toContain("tmdb-secret-1"); expect(text).not.toContain("brave-1"); }); });