BangSo/apps/api/src/persist-memory-provider-con...

237 lines
7.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { persistMemoryProviderConfig, updateMemoryProviderDefaultScope } from "./router.js";
const actor = {
userId: "user-1",
spaceId: "ws-1",
email: "a@b.com",
isDeploymentOwner: false,
};
function makeDeps(
overrides: {
existing?: { id: string; secretId: string } | null;
upsertResult?: {
provider: string;
settings: Record<string, string>;
defaultMemoryScope: string;
updatedAt: Date;
};
updateResult?: {
provider: string;
settings: Record<string, string>;
defaultMemoryScope: string;
updatedAt: Date;
};
spaceOwner?: boolean;
memberRole?: string;
} = {},
) {
const secretCreate = vi.fn().mockResolvedValue({ id: "secret-new" });
const secretDeleteMany = vi.fn().mockResolvedValue({ count: 1 });
const findUnique = vi.fn().mockResolvedValue(overrides.existing ?? null);
const upsert = vi.fn().mockResolvedValue(
overrides.upsertResult ?? {
provider: "supermemory",
settings: { mode: "cloud", baseUrl: "https://api.supermemory.ai" },
defaultMemoryScope: "isolated",
updatedAt: new Date("2026-08-19T00:00:00.000Z"),
},
);
const update = vi.fn().mockResolvedValue(
overrides.updateResult ?? {
provider: "supermemory",
settings: { mode: "cloud", baseUrl: "https://api.supermemory.ai" },
defaultMemoryScope: "shared",
updatedAt: new Date("2026-08-20T00:00:00.000Z"),
},
);
const prisma = {
spaceMember: {
findUnique: vi
.fn()
.mockResolvedValue(
overrides.spaceOwner === false ? null : { role: overrides.memberRole ?? "owner" },
),
},
spaceMemoryConfig: { findUnique, update, upsert },
secret: { create: secretCreate, deleteMany: secretDeleteMany },
$transaction: vi.fn(),
};
prisma.$transaction.mockImplementation(async (callback: (tx: typeof prisma) => unknown) =>
callback(prisma),
);
const deps = {
prisma,
secrets: { put: vi.fn().mockResolvedValue({ id: "secret-new", ciphertext: "cipher" }) },
};
return {
deps,
secretCreate,
secretDeleteMany,
findUnique,
upsert,
update,
transaction: prisma.$transaction,
};
}
function connectionInput(mode: "cloud" | "local", baseUrl?: string) {
return {
provider: "supermemory",
settings: { mode, ...(baseUrl ? { baseUrl } : {}) },
credentials: { apiKey: "sm_test_key_12345" },
defaultMemoryScope: "isolated" as const,
};
}
describe("persistMemoryProviderConfig", () => {
it("rejects non-owners before probing or writing Space configuration", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { deps, upsert } = makeDeps({ spaceOwner: false });
await expect(
persistMemoryProviderConfig(deps as never, actor, connectionInput("cloud")),
).rejects.toThrow();
expect(fetchMock).not.toHaveBeenCalled();
expect(upsert).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
it("rejects local mode without a baseUrl, without touching the database", async () => {
const { deps, upsert } = makeDeps();
await expect(
persistMemoryProviderConfig(deps as never, actor, connectionInput("local")),
).rejects.toThrow(/baseUrl/);
expect(upsert).not.toHaveBeenCalled();
});
it("rejects a non-loopback baseUrl in local mode without probing or touching the database", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { deps, upsert } = makeDeps();
await expect(
persistMemoryProviderConfig(
deps as never,
actor,
connectionInput("local", "http://169.254.169.254/latest/meta-data/"),
),
).rejects.toThrow(/loopback/);
expect(fetchMock).not.toHaveBeenCalled();
expect(upsert).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
it("probes before persisting, and rejects (without writing) when the probe fails", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("", { status: 401 })));
const { deps, upsert } = makeDeps();
await expect(
persistMemoryProviderConfig(deps as never, actor, {
...connectionInput("local", "http://localhost:6767"),
credentials: { apiKey: "sm_bad_key" },
}),
).rejects.toThrow();
expect(upsert).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
it("accepts a bracketed IPv6 loopback base URL in local mode", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response("[]", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const { deps, upsert } = makeDeps();
await persistMemoryProviderConfig(
deps as never,
actor,
connectionInput("local", "http://[::1]:6767"),
);
expect(fetchMock.mock.calls[0]![0]).toBe("http://[::1]:6767/v3/container-tags/list");
expect(upsert).toHaveBeenCalled();
vi.unstubAllGlobals();
});
it("connects cloud mode, defaulting the base URL, and returns the serialized config", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("[]", { status: 200 })));
const { deps, upsert, transaction } = makeDeps();
const result = await persistMemoryProviderConfig(
deps as never,
actor,
connectionInput("cloud"),
);
expect(upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { spaceId: "ws-1" },
create: expect.objectContaining({
provider: "supermemory",
settings: { mode: "cloud", baseUrl: "https://api.supermemory.ai" },
}),
}),
);
expect(result).toEqual({
provider: "supermemory",
settings: { mode: "cloud", baseUrl: "https://api.supermemory.ai" },
defaultMemoryScope: "isolated",
updatedAt: "2026-08-19T00:00:00.000Z",
});
expect(transaction).toHaveBeenCalledOnce();
vi.unstubAllGlobals();
});
it("deletes the old secret when replacing an existing config with a new key", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("[]", { status: 200 })));
const { deps, secretDeleteMany } = makeDeps({
existing: { id: "cfg-1", secretId: "secret-old" },
});
await persistMemoryProviderConfig(deps as never, actor, {
...connectionInput("cloud"),
credentials: { apiKey: "sm_new_key_12345" },
});
expect(secretDeleteMany).toHaveBeenCalledWith({ where: { id: "secret-old" } });
vi.unstubAllGlobals();
});
});
describe("updateMemoryProviderDefaultScope", () => {
it("accepts owners with additional Better Auth roles", async () => {
const { deps, update } = makeDeps({
existing: { id: "cfg-1", secretId: "secret-existing" },
memberRole: "owner,admin",
});
await updateMemoryProviderDefaultScope(deps as never, actor, "shared");
expect(update).toHaveBeenCalled();
});
it("updates only the generic scope setting and retains the provider secret", async () => {
const { deps, update, secretCreate, secretDeleteMany } = makeDeps({
existing: { id: "cfg-1", secretId: "secret-existing" },
});
const result = await updateMemoryProviderDefaultScope(deps as never, actor, "shared");
expect(update).toHaveBeenCalledWith({
where: { id: "cfg-1" },
data: { defaultMemoryScope: "shared" },
});
expect(secretCreate).not.toHaveBeenCalled();
expect(secretDeleteMany).not.toHaveBeenCalled();
expect(result.defaultMemoryScope).toBe("shared");
});
it("rejects non-owners without updating provider configuration", async () => {
const { deps, update } = makeDeps({
existing: { id: "cfg-1", secretId: "secret-existing" },
spaceOwner: false,
});
await expect(
updateMemoryProviderDefaultScope(deps as never, actor, "shared"),
).rejects.toThrow();
expect(update).not.toHaveBeenCalled();
});
});