test(api): cover shared resource access rules
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
assertCanReadResource,
|
||||
canReadAllResources,
|
||||
findOwnedResourceId,
|
||||
resolveResourcePermissions,
|
||||
} from "../lib/resource-access.js";
|
||||
|
||||
describe("resource access helpers", () => {
|
||||
it("returns no permissions without a db user", () => {
|
||||
expect(resolveResourcePermissions({ dbUser: null, roleDefaults: null })).toEqual(new Set());
|
||||
});
|
||||
|
||||
it("treats managers with resource permissions as staff readers", () => {
|
||||
const permissions = resolveResourcePermissions({
|
||||
dbUser: {
|
||||
systemRole: SystemRole.MANAGER,
|
||||
permissionOverrides: null,
|
||||
} as never,
|
||||
roleDefaults: null,
|
||||
});
|
||||
|
||||
expect(permissions.has(PermissionKey.VIEW_ALL_RESOURCES)).toBe(true);
|
||||
expect(canReadAllResources({
|
||||
dbUser: {
|
||||
systemRole: SystemRole.MANAGER,
|
||||
permissionOverrides: null,
|
||||
} as never,
|
||||
roleDefaults: null,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it("returns null when no linked resource lookup is possible", async () => {
|
||||
await expect(findOwnedResourceId({
|
||||
dbUser: { id: "user_1" } as never,
|
||||
roleDefaults: null,
|
||||
db: {},
|
||||
})).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("returns the owned resource id when the lookup succeeds", async () => {
|
||||
const findFirst = vi.fn().mockResolvedValue({ id: "res_1" });
|
||||
|
||||
await expect(findOwnedResourceId({
|
||||
dbUser: { id: "user_1" } as never,
|
||||
roleDefaults: null,
|
||||
db: {
|
||||
resource: {
|
||||
findFirst,
|
||||
},
|
||||
} as never,
|
||||
})).resolves.toBe("res_1");
|
||||
|
||||
expect(findFirst).toHaveBeenCalledWith({
|
||||
where: { userId: "user_1" },
|
||||
select: { id: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows staff readers to access arbitrary resources without ownership lookup", async () => {
|
||||
const findFirst = vi.fn();
|
||||
|
||||
await expect(assertCanReadResource({
|
||||
dbUser: {
|
||||
id: "mgr_1",
|
||||
systemRole: SystemRole.MANAGER,
|
||||
permissionOverrides: null,
|
||||
} as never,
|
||||
roleDefaults: null,
|
||||
db: {
|
||||
resource: {
|
||||
findFirst,
|
||||
},
|
||||
} as never,
|
||||
}, "res_1")).resolves.toBeUndefined();
|
||||
|
||||
expect(findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects non-owned resources for regular users", async () => {
|
||||
const findFirst = vi.fn().mockResolvedValue({ id: "res_own" });
|
||||
|
||||
await expect(assertCanReadResource({
|
||||
dbUser: {
|
||||
id: "user_1",
|
||||
systemRole: SystemRole.USER,
|
||||
permissionOverrides: null,
|
||||
} as never,
|
||||
roleDefaults: null,
|
||||
db: {
|
||||
resource: {
|
||||
findFirst,
|
||||
},
|
||||
} as never,
|
||||
}, "res_other", "custom message")).rejects.toEqual(expect.objectContaining<Partial<TRPCError>>({
|
||||
code: "FORBIDDEN",
|
||||
message: "custom message",
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -136,7 +136,7 @@ describe("resource router authorization", () => {
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(findUnique).toHaveBeenCalledWith({
|
||||
where: { email: "user@example.com" },
|
||||
where: { id: "user_1" },
|
||||
select: {
|
||||
resource: {
|
||||
select: {
|
||||
@@ -172,4 +172,149 @@ describe("resource router authorization", () => {
|
||||
expect(getAnonymizationDirectory).toHaveBeenCalledOnce();
|
||||
expect(anonymizeResource).toHaveBeenCalledWith(resource, null);
|
||||
});
|
||||
|
||||
it("uses the db user id for self-service resource lookups even when the session email is stale", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({ resource: null });
|
||||
const caller = createCaller({
|
||||
session: {
|
||||
user: { email: "stale@example.com", name: "User", image: null },
|
||||
expires: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
db: {
|
||||
user: {
|
||||
findUnique,
|
||||
},
|
||||
} as never,
|
||||
dbUser: {
|
||||
id: "user_1",
|
||||
systemRole: SystemRole.USER,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
roleDefaults: null,
|
||||
});
|
||||
|
||||
await caller.getMyResource();
|
||||
|
||||
expect(findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user_1" },
|
||||
select: {
|
||||
resource: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
eid: true,
|
||||
chapter: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("requires authentication for hover-card lookups", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
resource: {
|
||||
findUnique,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
await expect(caller.getHoverCard({ id: "res_1" })).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks regular users from manager-only skill imports for arbitrary resources", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
resource: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(
|
||||
caller.importSkillMatrixForResource({
|
||||
resourceId: "res_1",
|
||||
skills: [{ skill: "Houdini", proficiency: 5 }],
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for manager-only skill imports for arbitrary resources", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
resource: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
}, { role: SystemRole.MANAGER, session: false }));
|
||||
|
||||
await expect(
|
||||
caller.importSkillMatrixForResource({
|
||||
resourceId: "res_1",
|
||||
skills: [{ skill: "Houdini", proficiency: 5 }],
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks non-admin users from batch skill matrix imports", async () => {
|
||||
const findMany = vi.fn();
|
||||
const transaction = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
resource: {
|
||||
findMany,
|
||||
},
|
||||
$transaction: transaction,
|
||||
}, { role: SystemRole.MANAGER }));
|
||||
|
||||
await expect(
|
||||
caller.batchImportSkillMatrices({
|
||||
entries: [{ eid: "E-001", skills: [{ skill: "Maya", proficiency: 4 }] }],
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
});
|
||||
|
||||
expect(findMany).not.toHaveBeenCalled();
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for admin-only batch skill matrix imports", async () => {
|
||||
const findMany = vi.fn();
|
||||
const transaction = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
resource: {
|
||||
findMany,
|
||||
},
|
||||
$transaction: transaction,
|
||||
}, { role: SystemRole.ADMIN, session: false }));
|
||||
|
||||
await expect(
|
||||
caller.batchImportSkillMatrices({
|
||||
entries: [{ eid: "E-001", skills: [{ skill: "Maya", proficiency: 4 }] }],
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findMany).not.toHaveBeenCalled();
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,23 @@ vi.mock("../router/blueprint-validation.js", () => ({
|
||||
assertBlueprintDynamicFields: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../ai-client.js", () => ({
|
||||
createAiClient: vi.fn(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn().mockResolvedValue({
|
||||
choices: [{ message: { content: "Generated summary" } }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
})),
|
||||
isAiConfigured: vi.fn().mockReturnValue(true),
|
||||
loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/anonymization.js", () => ({
|
||||
anonymizeResource: vi.fn((r: Record<string, unknown>) => r),
|
||||
anonymizeResources: vi.fn((rs: unknown[]) => rs),
|
||||
@@ -24,7 +41,18 @@ vi.mock("../lib/anonymization.js", () => ({
|
||||
resolveResourceIdsByDisplayedEids: vi.fn().mockResolvedValue(new Map()),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/logger.js", () => ({
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { resourceRouter } from "../router/resource.js";
|
||||
import { createAiClient, isAiConfigured, loggedAiCall } from "../ai-client.js";
|
||||
import { getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
import { createCallerFactory } from "../trpc.js";
|
||||
|
||||
const createCaller = createCallerFactory(resourceRouter);
|
||||
@@ -44,6 +72,21 @@ function createManagerCaller(db: Record<string, unknown>) {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createProtectedCaller(db: Record<string, unknown>) {
|
||||
return createCaller({
|
||||
session: {
|
||||
@@ -221,6 +264,65 @@ describe("resource router CRUD", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getByEid", () => {
|
||||
it("returns the matching resource for staff callers", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(sampleResource),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.getByEid({ eid: "E-001" });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: "res_1",
|
||||
displayName: "Alice",
|
||||
eid: "E-001",
|
||||
});
|
||||
expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { eid: "E-001" } });
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when the requested eid does not exist", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
await expect(caller.getByEid({ eid: "E-404" })).rejects.toThrow(
|
||||
expect.objectContaining({ code: "NOT_FOUND" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves anonymized alias eids through the directory fallback", async () => {
|
||||
vi.mocked(getAnonymizationDirectory).mockResolvedValueOnce({
|
||||
byAliasEid: new Map([["alias-e-001", "res_1"]]),
|
||||
} as Awaited<ReturnType<typeof getAnonymizationDirectory>>);
|
||||
|
||||
const findUnique = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(sampleResource);
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.getByEid({ eid: "alias-e-001" });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: "res_1",
|
||||
eid: "E-001",
|
||||
});
|
||||
expect(findUnique).toHaveBeenNthCalledWith(1, { where: { eid: "alias-e-001" } });
|
||||
expect(findUnique).toHaveBeenNthCalledWith(2, { where: { id: "res_1" } });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── create ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe("create", () => {
|
||||
@@ -361,6 +463,60 @@ describe("resource router CRUD", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("batchDeactivate", () => {
|
||||
it("deactivates all requested resources and records one audit entry", async () => {
|
||||
const update = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false })
|
||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false });
|
||||
const db = {
|
||||
resource: {
|
||||
update,
|
||||
},
|
||||
$transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
|
||||
auditLog: { create: vi.fn().mockResolvedValue({}) },
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.batchDeactivate({ ids: ["res_1", "res_2"] });
|
||||
|
||||
expect(result).toEqual({ count: 2 });
|
||||
expect(db.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(db.auditLog.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
entityType: "Resource",
|
||||
action: "UPDATE",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chapters", () => {
|
||||
it("returns active chapter names in ascending order", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ chapter: "Animation" },
|
||||
{ chapter: "CGI" },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const result = await caller.chapters();
|
||||
|
||||
expect(result).toEqual(["Animation", "CGI"]);
|
||||
expect(db.resource.findMany).toHaveBeenCalledWith({
|
||||
where: { isActive: true, chapter: { not: null } },
|
||||
select: { chapter: true },
|
||||
distinct: ["chapter"],
|
||||
orderBy: { chapter: "asc" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getHoverCard ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("getHoverCard", () => {
|
||||
@@ -478,6 +634,10 @@ describe("resource router CRUD", () => {
|
||||
skills: [{ skill: "Maya", proficiency: 4 }],
|
||||
});
|
||||
|
||||
expect(db.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user_1" },
|
||||
include: { resource: true },
|
||||
});
|
||||
expect(db.resource.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "res_1" },
|
||||
@@ -502,6 +662,171 @@ describe("resource router CRUD", () => {
|
||||
skills: [{ skill: "Nuke", proficiency: 3 }],
|
||||
}),
|
||||
).rejects.toThrow("No resource linked to your account");
|
||||
expect(db.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user_1" },
|
||||
include: { resource: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("importSkillMatrixForResource", () => {
|
||||
it("updates skills for an existing resource", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(sampleResource),
|
||||
update: vi.fn().mockResolvedValue(sampleResource),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.importSkillMatrixForResource({
|
||||
resourceId: "res_1",
|
||||
skills: [{ skill: "Houdini", proficiency: 5 }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ count: 1 });
|
||||
expect(db.resource.findUnique).toHaveBeenCalledWith({ where: { id: "res_1" } });
|
||||
expect(db.resource.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "res_1" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when the target resource does not exist", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
update: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
await expect(
|
||||
caller.importSkillMatrixForResource({
|
||||
resourceId: "missing",
|
||||
skills: [{ skill: "Houdini", proficiency: 5 }],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
expect.objectContaining({ code: "NOT_FOUND" }),
|
||||
);
|
||||
expect(db.resource.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("batchImportSkillMatrices", () => {
|
||||
it("updates all matching resources in one transaction and reports missing eids", async () => {
|
||||
const update = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_1" })
|
||||
.mockResolvedValueOnce({ ...sampleResource, id: "res_2", eid: "E-002" });
|
||||
const db = {
|
||||
resource: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ id: "res_1", eid: "E-001" },
|
||||
{ id: "res_2", eid: "E-002" },
|
||||
]),
|
||||
update,
|
||||
},
|
||||
$transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
|
||||
};
|
||||
|
||||
const caller = createAdminCaller(db);
|
||||
const result = await caller.batchImportSkillMatrices({
|
||||
entries: [
|
||||
{ eid: "E-001", skills: [{ skill: "Houdini", proficiency: 5 }] },
|
||||
{ eid: "E-002", skills: [{ skill: "Nuke", proficiency: 4 }] },
|
||||
{ eid: "E-404", skills: [{ skill: "Maya", proficiency: 3 }] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ updated: 2, notFound: 1 });
|
||||
expect(db.resource.findMany).toHaveBeenCalledWith({
|
||||
where: { eid: { in: ["E-001", "E-002", "E-404"] } },
|
||||
select: { id: true, eid: true },
|
||||
});
|
||||
expect(db.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(db.resource.update).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateAiSummary", () => {
|
||||
it("persists the generated summary for the requested resource", async () => {
|
||||
const update = vi.fn().mockResolvedValue({
|
||||
...sampleResource,
|
||||
aiSummary: "Generated summary",
|
||||
});
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
...sampleResource,
|
||||
skills: [
|
||||
{ skill: "Maya", proficiency: 5, isMainSkill: true },
|
||||
{ skill: "Nuke", proficiency: 4 },
|
||||
],
|
||||
areaRole: { name: "3D Artist" },
|
||||
}),
|
||||
update,
|
||||
},
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "singleton",
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-test",
|
||||
aiSummaryPrompt: null,
|
||||
aiMaxCompletionTokens: 200,
|
||||
aiTemperature: 0.7,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.generateAiSummary({ resourceId: "res_1" });
|
||||
|
||||
expect(isAiConfigured).toHaveBeenCalledWith(expect.objectContaining({
|
||||
azureOpenAiDeployment: "gpt-test",
|
||||
}));
|
||||
expect(createAiClient).toHaveBeenCalledWith(expect.objectContaining({
|
||||
azureOpenAiDeployment: "gpt-test",
|
||||
}));
|
||||
expect(loggedAiCall).toHaveBeenCalledOnce();
|
||||
expect(update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "res_1" },
|
||||
data: expect.objectContaining({
|
||||
aiSummary: "Generated summary",
|
||||
aiSummaryUpdatedAt: expect.any(Date),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ summary: "Generated summary" });
|
||||
});
|
||||
|
||||
it("fails fast when AI is not configured", async () => {
|
||||
vi.mocked(isAiConfigured).mockReturnValueOnce(false);
|
||||
|
||||
const db = {
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
...sampleResource,
|
||||
areaRole: { name: "3D Artist" },
|
||||
}),
|
||||
},
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "singleton",
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: null,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.generateAiSummary({ resourceId: "res_1" })).rejects.toThrow(
|
||||
"AI is not configured. Please set credentials in Admin → Settings.",
|
||||
);
|
||||
expect(createAiClient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user