234 lines
6.6 KiB
TypeScript
234 lines
6.6 KiB
TypeScript
import { SystemRole } from "@capakraken/shared";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { parseAiError } from "../ai-client.js";
|
|
import { logger } from "../lib/logger.js";
|
|
import { settingsRouter } from "../router/settings.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
vi.mock("../lib/logger.js", () => ({
|
|
logger: {
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const createCaller = createCallerFactory(settingsRouter);
|
|
|
|
function createAdminCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "admin@example.com", name: "Admin", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "admin_1",
|
|
systemRole: SystemRole.ADMIN,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("runtime config hardening", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("sanitizes AI fallback diagnostics", () => {
|
|
const error = parseAiError(
|
|
new Error(
|
|
"Provider failed at https://example.openai.azure.com/path?api-key=topsecret with Bearer sk-super-secret",
|
|
),
|
|
);
|
|
|
|
expect(error).toContain("<redacted-url>");
|
|
expect(error).toContain("<redacted-secret>");
|
|
expect(error).not.toContain("https://example.openai.azure.com");
|
|
expect(error).not.toContain("sk-super-secret");
|
|
expect(error).not.toContain("topsecret");
|
|
});
|
|
|
|
it("does not expose raw diagnostics from AI connection tests", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
aiProvider: "openai",
|
|
azureOpenAiDeployment: "gpt-4o-mini",
|
|
azureOpenAiApiKey: "sk-live-secret",
|
|
});
|
|
|
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
text: vi.fn().mockResolvedValue(
|
|
JSON.stringify({
|
|
error: {
|
|
message: "upstream failure at https://api.openai.com/v1/chat/completions?api-key=hidden",
|
|
},
|
|
}),
|
|
),
|
|
}));
|
|
|
|
const caller = createAdminCaller({
|
|
systemSettings: {
|
|
findUnique,
|
|
},
|
|
});
|
|
|
|
const result = await caller.testAiConnection();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result).not.toHaveProperty("raw");
|
|
expect(result.error).toContain("<redacted-url>");
|
|
expect(result.error).not.toContain("https://api.openai.com");
|
|
expect(result.error).not.toContain("hidden");
|
|
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "openai",
|
|
diagnostic: expect.stringContaining("<redacted-url>"),
|
|
}),
|
|
"AI connection test failed",
|
|
);
|
|
});
|
|
|
|
it("reports secret presence flags when secrets come from environment overrides", async () => {
|
|
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
|
vi.stubEnv("SMTP_PASSWORD", "env-smtp-password");
|
|
vi.stubEnv("GEMINI_API_KEY", "env-gemini-key");
|
|
|
|
const caller = createAdminCaller({
|
|
systemSettings: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
aiProvider: "openai",
|
|
azureOpenAiApiKey: null,
|
|
smtpPassword: null,
|
|
geminiApiKey: null,
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await caller.getSystemSettings();
|
|
|
|
expect(result.hasApiKey).toBe(true);
|
|
expect(result.hasSmtpPassword).toBe(true);
|
|
expect(result.hasGeminiApiKey).toBe(true);
|
|
expect(result.runtimeSecrets.azureOpenAiApiKey.activeSource).toBe("environment");
|
|
expect(result.runtimeSecrets.smtpPassword.activeSource).toBe("environment");
|
|
expect(result.legacyStoredSecretFields).toEqual([]);
|
|
});
|
|
|
|
it("prefers environment API keys during AI connection tests", async () => {
|
|
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
|
const fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
text: vi.fn().mockResolvedValue("ok"),
|
|
});
|
|
vi.stubGlobal("fetch", fetch);
|
|
|
|
const caller = createAdminCaller({
|
|
systemSettings: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
aiProvider: "openai",
|
|
azureOpenAiDeployment: "gpt-4o-mini",
|
|
azureOpenAiApiKey: null,
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await caller.testAiConnection();
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
"https://api.openai.com/v1/chat/completions",
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: "Bearer env-openai-key",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not persist incoming secret fields through updateSystemSettings", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const upsert = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
systemSettings: {
|
|
findUnique,
|
|
upsert,
|
|
},
|
|
});
|
|
|
|
const result = await caller.updateSystemSettings({
|
|
azureOpenAiApiKey: "sk-should-not-store",
|
|
smtpPassword: "smtp-should-not-store",
|
|
geminiApiKey: "gemini-should-not-store",
|
|
azureDalleApiKey: "dalle-should-not-store",
|
|
anonymizationSeed: "seed-should-not-store",
|
|
aiProvider: "openai",
|
|
azureOpenAiDeployment: "gpt-4o-mini",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
ignoredSecretFields: [
|
|
"azureOpenAiApiKey",
|
|
"smtpPassword",
|
|
"anonymizationSeed",
|
|
"azureDalleApiKey",
|
|
"geminiApiKey",
|
|
],
|
|
secretStorageMode: "environment-only",
|
|
});
|
|
expect(upsert).toHaveBeenCalledWith({
|
|
where: { id: "singleton" },
|
|
create: {
|
|
id: "singleton",
|
|
aiProvider: "openai",
|
|
azureOpenAiDeployment: "gpt-4o-mini",
|
|
},
|
|
update: {
|
|
aiProvider: "openai",
|
|
azureOpenAiDeployment: "gpt-4o-mini",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("can clear legacy runtime secrets from database storage", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
azureOpenAiApiKey: "db-key",
|
|
azureDalleApiKey: null,
|
|
geminiApiKey: "db-gemini",
|
|
smtpPassword: "db-smtp",
|
|
anonymizationSeed: null,
|
|
});
|
|
const update = vi.fn().mockResolvedValue({});
|
|
const caller = createAdminCaller({
|
|
systemSettings: {
|
|
findUnique,
|
|
update,
|
|
},
|
|
});
|
|
|
|
const result = await caller.clearStoredRuntimeSecrets();
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "smtpPassword"],
|
|
});
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: "singleton" },
|
|
data: {
|
|
azureOpenAiApiKey: null,
|
|
geminiApiKey: null,
|
|
smtpPassword: null,
|
|
},
|
|
});
|
|
});
|
|
});
|