test(api): cover assistant settings tools
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import { createToolContext, executeTool } from "./assistant-tools-settings-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant settings tool get_ai_configured", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports AI configuration only to admin users through the real settings path", async () => {
|
||||||
|
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||||
|
|
||||||
|
const ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiDeployment: "gpt-4o-mini",
|
||||||
|
azureOpenAiApiKey: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool("get_ai_configured", JSON.stringify({}), ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({ configured: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects get_ai_configured for non-admin assistant users", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemRole.MANAGER,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeTool("get_ai_configured", JSON.stringify({}), ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
error: "You do not have permission to perform this action.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { DEFAULT_OPENAI_MODEL, SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createToolContext,
|
||||||
|
executeTool,
|
||||||
|
generateGeminiImage,
|
||||||
|
testSmtpConnection,
|
||||||
|
} from "./assistant-tools-settings-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant settings tools connection checks", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tests the AI connection through the real settings router path", 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 ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiDeployment: DEFAULT_OPENAI_MODEL,
|
||||||
|
azureOpenAiApiKey: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool("test_ai_connection", JSON.stringify({}), ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({ ok: true });
|
||||||
|
expect(fetch).toHaveBeenCalledWith(
|
||||||
|
"https://api.openai.com/v1/chat/completions",
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
Authorization: "Bearer env-openai-key",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tests the SMTP connection through the real settings router path", async () => {
|
||||||
|
vi.mocked(testSmtpConnection).mockResolvedValue({ ok: true });
|
||||||
|
|
||||||
|
const ctx = createToolContext({});
|
||||||
|
const result = await executeTool("test_smtp_connection", JSON.stringify({}), ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({ ok: true });
|
||||||
|
expect(testSmtpConnection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tests the Gemini connection through the real settings router path", async () => {
|
||||||
|
vi.mocked(generateGeminiImage).mockResolvedValue("data:image/png;base64,abc123");
|
||||||
|
|
||||||
|
const ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
|
geminiApiKey: "gemini-key",
|
||||||
|
geminiModel: "gemini-2.5-flash-image",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool("test_gemini_connection", JSON.stringify({}), ctx);
|
||||||
|
const parsed = JSON.parse(result.content) as {
|
||||||
|
ok: boolean;
|
||||||
|
model: string;
|
||||||
|
preview: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parsed.ok).toBe(true);
|
||||||
|
expect(parsed.model).toBe("gemini-2.5-flash-image");
|
||||||
|
expect(parsed.preview).toContain("data:image/png;base64,abc123");
|
||||||
|
expect(generateGeminiImage).toHaveBeenCalledWith(
|
||||||
|
"gemini-key",
|
||||||
|
"A simple blue circle on white background, minimal, 256x256",
|
||||||
|
"gemini-2.5-flash-image",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects admin-only connection tools for non-admin assistant users", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemRole.MANAGER,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const toolName of [
|
||||||
|
"test_ai_connection",
|
||||||
|
"test_smtp_connection",
|
||||||
|
"test_gemini_connection",
|
||||||
|
] as const) {
|
||||||
|
const result = await executeTool(toolName, JSON.stringify({}), ctx);
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
error: "You do not have permission to perform this action.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { DEFAULT_OPENAI_MODEL, SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import { createToolContext, executeTool } from "./assistant-tools-settings-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant settings tools runtime settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns system settings with runtime secret flags through the real settings router path", 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 ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiApiKey: null,
|
||||||
|
smtpPassword: null,
|
||||||
|
geminiApiKey: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool("get_system_settings", JSON.stringify({}), ctx);
|
||||||
|
const parsed = JSON.parse(result.content) as {
|
||||||
|
hasApiKey: boolean;
|
||||||
|
hasSmtpPassword: boolean;
|
||||||
|
hasGeminiApiKey: boolean;
|
||||||
|
runtimeSecrets: {
|
||||||
|
azureOpenAiApiKey: { activeSource: string };
|
||||||
|
smtpPassword: { activeSource: string };
|
||||||
|
};
|
||||||
|
legacyStoredSecretFields: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parsed.hasApiKey).toBe(true);
|
||||||
|
expect(parsed.hasSmtpPassword).toBe(true);
|
||||||
|
expect(parsed.hasGeminiApiKey).toBe(true);
|
||||||
|
expect(parsed.runtimeSecrets.azureOpenAiApiKey.activeSource).toBe("environment");
|
||||||
|
expect(parsed.runtimeSecrets.smtpPassword.activeSource).toBe("environment");
|
||||||
|
expect(parsed.legacyStoredSecretFields).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates system settings without persisting incoming secret fields", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
const upsert = vi.fn().mockResolvedValue({});
|
||||||
|
const ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique,
|
||||||
|
upsert,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"update_system_settings",
|
||||||
|
JSON.stringify({
|
||||||
|
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: DEFAULT_OPENAI_MODEL,
|
||||||
|
}),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
ok: true,
|
||||||
|
ignoredSecretFields: [
|
||||||
|
"azureOpenAiApiKey",
|
||||||
|
"smtpPassword",
|
||||||
|
"anonymizationSeed",
|
||||||
|
"azureDalleApiKey",
|
||||||
|
"geminiApiKey",
|
||||||
|
],
|
||||||
|
secretStorageMode: "environment-only",
|
||||||
|
});
|
||||||
|
expect(findUnique).toHaveBeenCalledWith({ where: { id: "singleton" } });
|
||||||
|
expect(upsert).toHaveBeenCalledWith({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
create: {
|
||||||
|
id: "singleton",
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiDeployment: DEFAULT_OPENAI_MODEL,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiDeployment: DEFAULT_OPENAI_MODEL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears legacy runtime secrets through the real settings router path", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue({
|
||||||
|
azureOpenAiApiKey: "db-openai",
|
||||||
|
azureDalleApiKey: null,
|
||||||
|
geminiApiKey: "db-gemini",
|
||||||
|
smtpPassword: null,
|
||||||
|
anonymizationSeed: "db-seed",
|
||||||
|
});
|
||||||
|
const update = vi.fn().mockResolvedValue({ id: "singleton" });
|
||||||
|
const auditCreate = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const ctx = createToolContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique,
|
||||||
|
update,
|
||||||
|
},
|
||||||
|
auditLog: {
|
||||||
|
create: auditCreate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool("clear_stored_runtime_secrets", "{}", ctx);
|
||||||
|
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
ok: true,
|
||||||
|
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "anonymizationSeed"],
|
||||||
|
});
|
||||||
|
expect(update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
data: {
|
||||||
|
azureOpenAiApiKey: null,
|
||||||
|
geminiApiKey: null,
|
||||||
|
anonymizationSeed: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(auditCreate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects admin-only runtime settings tools for non-admin assistant users", async () => {
|
||||||
|
const ctx = createToolContext(
|
||||||
|
{
|
||||||
|
systemSettings: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
upsert: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemRole.MANAGER,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [toolName, payload] of [
|
||||||
|
["get_system_settings", {}],
|
||||||
|
["update_system_settings", { aiProvider: "openai" }],
|
||||||
|
["clear_stored_runtime_secrets", {}],
|
||||||
|
] as const) {
|
||||||
|
const result = await executeTool(toolName, JSON.stringify(payload), ctx);
|
||||||
|
expect(JSON.parse(result.content)).toEqual({
|
||||||
|
error: "You do not have permission to perform this action.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getDashboardBudgetForecast: vi.fn().mockResolvedValue([]),
|
||||||
|
getDashboardPeakTimes: vi.fn().mockResolvedValue([]),
|
||||||
|
listAssignmentBookings: vi.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../lib/logger.js", () => ({
|
||||||
|
logger: {
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../lib/email.js", () => ({
|
||||||
|
testSmtpConnection: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../gemini-client.js", () => ({
|
||||||
|
generateGeminiImage: vi.fn(),
|
||||||
|
parseGeminiError: vi.fn((err: unknown) => (err instanceof Error ? err.message : String(err))),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { testSmtpConnection as testSmtpConnectionMock } from "../lib/email.js";
|
||||||
|
import { generateGeminiImage as generateGeminiImageMock } from "../gemini-client.js";
|
||||||
|
import { executeTool as executeAssistantTool, type ToolContext } from "../router/assistant-tools.js";
|
||||||
|
|
||||||
|
export const executeTool = executeAssistantTool;
|
||||||
|
export const testSmtpConnection = testSmtpConnectionMock;
|
||||||
|
export const generateGeminiImage = generateGeminiImageMock;
|
||||||
|
|
||||||
|
export function createToolContext(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
userRole: SystemRole = SystemRole.ADMIN,
|
||||||
|
): ToolContext {
|
||||||
|
return {
|
||||||
|
db: db as ToolContext["db"],
|
||||||
|
userId: "admin_1",
|
||||||
|
userRole,
|
||||||
|
permissions: new Set(),
|
||||||
|
session: {
|
||||||
|
user: { email: "admin@example.com", name: "Admin Assistant", image: null },
|
||||||
|
expires: "2026-03-30T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
dbUser: {
|
||||||
|
id: "admin_1",
|
||||||
|
systemRole: userRole,
|
||||||
|
permissionOverrides: null,
|
||||||
|
},
|
||||||
|
roleDefaults: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user