mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the logic code for combos pages
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getModelAliases = vi.fn();
|
||||
const getProviderConnections = vi.fn();
|
||||
const getCustomModels = vi.fn();
|
||||
const getProviderNodes = vi.fn();
|
||||
const getUsers = vi.fn();
|
||||
const getDisabledModels = vi.fn();
|
||||
const requireUsageDashboardUser = vi.fn();
|
||||
const getCapabilitiesForModel = vi.fn();
|
||||
|
||||
vi.mock("@/models", () => ({
|
||||
getCustomModels,
|
||||
getModelAliases,
|
||||
getProviderConnections,
|
||||
getProviderNodes,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", () => ({ getUsers }));
|
||||
|
||||
vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels }));
|
||||
vi.mock("@/lib/auth/currentUser", () => ({
|
||||
requireUsageDashboardUser,
|
||||
}));
|
||||
vi.mock("@/shared/constants/models", () => ({
|
||||
AI_MODELS: [
|
||||
{ provider: "alpha", model: "enabled", name: "Enabled model" },
|
||||
{ provider: "alpha", model: "disabled", name: "Disabled model" },
|
||||
{ provider: "beta", model: "inactive", name: "Inactive provider model" },
|
||||
],
|
||||
}));
|
||||
vi.mock("@/shared/constants/providers", () => {
|
||||
const providers = {
|
||||
alpha: { id: "alpha", alias: "alpha-alias", name: "Alpha", color: "#111111" },
|
||||
beta: { id: "beta", alias: "beta-alias", name: "Beta", color: "#222222" },
|
||||
};
|
||||
|
||||
return {
|
||||
AI_PROVIDERS: providers,
|
||||
getProviderAlias: (providerId) => providers[providerId]?.alias || providerId,
|
||||
getProviderByAlias: (providerId) => providers[providerId],
|
||||
isOpenAICompatibleProvider: (providerId) => providerId.startsWith("openai-compatible-"),
|
||||
isAnthropicCompatibleProvider: (providerId) => providerId.startsWith("anthropic-compatible-"),
|
||||
};
|
||||
});
|
||||
vi.mock("open-sse/providers/capabilities.js", () => ({ getCapabilitiesForModel }));
|
||||
|
||||
const { GET } = await import("../../src/app/api/models/connected/route.js");
|
||||
|
||||
describe("GET /api/models/connected", () => {
|
||||
beforeEach(() => {
|
||||
getModelAliases.mockReset();
|
||||
getProviderConnections.mockReset();
|
||||
getCustomModels.mockReset();
|
||||
getProviderNodes.mockReset();
|
||||
getUsers.mockReset();
|
||||
getDisabledModels.mockReset();
|
||||
requireUsageDashboardUser.mockReset();
|
||||
getCapabilitiesForModel.mockReset();
|
||||
|
||||
getModelAliases.mockResolvedValue({ "alpha/enabled": "preferred-alpha" });
|
||||
getDisabledModels.mockResolvedValue({ "alpha-alias": ["disabled"] });
|
||||
getCustomModels.mockResolvedValue([]);
|
||||
getProviderNodes.mockResolvedValue([]);
|
||||
getUsers.mockResolvedValue([{ id: "admin", role: "admin", isActive: true }]);
|
||||
getCapabilitiesForModel.mockReturnValue({ vision: false, search: true, reasoning: true });
|
||||
getProviderConnections.mockResolvedValue([
|
||||
{ provider: "alpha", isActive: true, apiKey: "secret" },
|
||||
{ provider: "beta", isActive: false, apiKey: "secret" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns every connected-provider model to an administrator, including disabled rows", async () => {
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([
|
||||
expect.objectContaining({
|
||||
fullModel: "alpha/disabled",
|
||||
providerAlias: "alpha-alias",
|
||||
disabled: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
fullModel: "alpha/enabled",
|
||||
alias: "preferred-alpha",
|
||||
disabled: false,
|
||||
caps: { vision: false, search: true, reasoning: true },
|
||||
}),
|
||||
]);
|
||||
expect(body.models).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ fullModel: "beta/inactive" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("excludes disabled models for non-administrators", async () => {
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([
|
||||
expect.objectContaining({ fullModel: "alpha/enabled", disabled: false }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes administrator-managed compatible-provider models for non-administrators", async () => {
|
||||
const providerId = "openai-compatible-test-node";
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
|
||||
getProviderConnections.mockResolvedValue([
|
||||
{
|
||||
provider: providerId,
|
||||
isActive: true,
|
||||
apiKey: "admin-secret",
|
||||
ownerId: "admin",
|
||||
providerSpecificData: { nodeName: "Company Gateway" },
|
||||
},
|
||||
]);
|
||||
getProviderNodes.mockResolvedValue([
|
||||
{ id: providerId, type: "openai-compatible", name: "Company Gateway" },
|
||||
]);
|
||||
getCustomModels.mockResolvedValue([
|
||||
{ providerAlias: providerId, id: "gpt-company", name: "Company GPT", type: "llm" },
|
||||
{ providerAlias: providerId, id: "company-embed", name: "Company Embed", type: "embedding" },
|
||||
]);
|
||||
getModelAliases.mockResolvedValue({ [`${providerId}/gpt-company`]: "company-chat" });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: expect.objectContaining({ id: providerId, name: "Company Gateway" }),
|
||||
providerAlias: providerId,
|
||||
model: "gpt-company",
|
||||
name: "Company GPT",
|
||||
fullModel: `${providerId}/gpt-company`,
|
||||
alias: "company-chat",
|
||||
isCustom: true,
|
||||
}),
|
||||
]);
|
||||
expect(body.models).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ model: "company-embed" }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("does not expose disabled compatible-provider models to non-administrators", async () => {
|
||||
const providerId = "anthropic-compatible-test-node";
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
|
||||
getProviderConnections.mockResolvedValue([
|
||||
{ provider: providerId, isActive: true, apiKey: "admin-secret", ownerId: "admin" },
|
||||
]);
|
||||
getProviderNodes.mockResolvedValue([
|
||||
{ id: providerId, type: "anthropic-compatible", name: "Company Anthropic" },
|
||||
]);
|
||||
getCustomModels.mockResolvedValue([
|
||||
{ providerAlias: providerId, id: "claude-company", type: "llm" },
|
||||
]);
|
||||
getDisabledModels.mockResolvedValue({ [providerId]: ["claude-company"] });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ fullModel: `${providerId}/claude-company` }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("does not treat a non-admin compatible-provider connection as shared", async () => {
|
||||
const providerId = "openai-compatible-user-node";
|
||||
requireUsageDashboardUser.mockResolvedValue({ id: "member-b", role: "user" });
|
||||
getUsers.mockResolvedValue([
|
||||
{ id: "admin", role: "admin", isActive: true },
|
||||
{ id: "member-a", role: "user", isActive: true },
|
||||
]);
|
||||
getProviderConnections.mockResolvedValue([
|
||||
{ provider: providerId, isActive: true, apiKey: "member-secret", ownerId: "member-a" },
|
||||
]);
|
||||
getProviderNodes.mockResolvedValue([
|
||||
{ id: providerId, type: "openai-compatible", name: "Member Gateway" },
|
||||
]);
|
||||
getCustomModels.mockResolvedValue([
|
||||
{ providerAlias: providerId, id: "member-only-model", type: "llm" },
|
||||
]);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.models).not.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ fullModel: `${providerId}/member-only-model` }),
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getCustomModels = vi.fn();
|
||||
const addCustomModel = vi.fn();
|
||||
const deleteCustomModel = vi.fn();
|
||||
const requireAdminUser = vi.fn();
|
||||
|
||||
vi.mock("@/models", () => ({
|
||||
getCustomModels,
|
||||
addCustomModel,
|
||||
deleteCustomModel,
|
||||
}));
|
||||
vi.mock("@/lib/auth/currentUser", () => ({ requireAdminUser }));
|
||||
|
||||
const { GET, POST, DELETE } = await import("../../src/app/api/models/custom/route.js");
|
||||
|
||||
describe("/api/models/custom", () => {
|
||||
beforeEach(() => {
|
||||
getCustomModels.mockReset();
|
||||
addCustomModel.mockReset();
|
||||
deleteCustomModel.mockReset();
|
||||
requireAdminUser.mockReset();
|
||||
});
|
||||
|
||||
it("keeps the shared catalog readable to authenticated model selectors", async () => {
|
||||
getCustomModels.mockResolvedValue([{ providerAlias: "openai", id: "gpt-test", type: "llm" }]);
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
models: [{ providerAlias: "openai", id: "gpt-test", type: "llm" }],
|
||||
});
|
||||
expect(requireAdminUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a non-admin adding a shared custom model", async () => {
|
||||
requireAdminUser.mockRejectedValue(new Error("Forbidden"));
|
||||
|
||||
const response = await POST(new Request("http://localhost/api/models/custom", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ providerAlias: "openai", id: "gpt-test", type: "llm" }),
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(addCustomModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows an admin to add a shared custom model", async () => {
|
||||
requireAdminUser.mockResolvedValue({ id: "admin", role: "admin" });
|
||||
addCustomModel.mockResolvedValue(true);
|
||||
|
||||
const response = await POST(new Request("http://localhost/api/models/custom", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ providerAlias: "openai", id: "gpt-test", type: "llm" }),
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(addCustomModel).toHaveBeenCalledWith({
|
||||
providerAlias: "openai",
|
||||
id: "gpt-test",
|
||||
type: "llm",
|
||||
name: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a non-admin deleting a shared custom model", async () => {
|
||||
requireAdminUser.mockRejectedValue(new Error("Forbidden"));
|
||||
|
||||
const response = await DELETE(new Request(
|
||||
"http://localhost/api/models/custom?providerAlias=openai&id=gpt-test&type=llm",
|
||||
{ method: "DELETE" },
|
||||
));
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(deleteCustomModel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getProviderConnectionById = vi.fn();
|
||||
const getProxyPoolById = vi.fn();
|
||||
const updateProviderConnection = vi.fn();
|
||||
const deleteProviderConnection = vi.fn();
|
||||
const getProviderConnectionAccess = vi.fn();
|
||||
|
||||
vi.mock("@/models", () => ({
|
||||
getProviderConnectionById,
|
||||
getProxyPoolById,
|
||||
updateProviderConnection,
|
||||
deleteProviderConnection,
|
||||
}));
|
||||
vi.mock("@/lib/providers/connectionAccess", () => ({ getProviderConnectionAccess }));
|
||||
vi.mock("@/shared/constants/providers", () => ({
|
||||
isOpenAICompatibleProvider: (provider) => provider.startsWith("openai-compatible-"),
|
||||
isAnthropicCompatibleProvider: (provider) => provider.startsWith("anthropic-compatible-"),
|
||||
isCustomEmbeddingProvider: (provider) => provider.startsWith("custom-embedding-"),
|
||||
}));
|
||||
|
||||
const { PUT, DELETE } = await import("../../src/app/api/providers/[id]/route.js");
|
||||
|
||||
const memberAccess = {
|
||||
user: { id: "member", role: "user" },
|
||||
ownerId: "member",
|
||||
};
|
||||
const adminAccess = {
|
||||
user: { id: "admin", role: "admin" },
|
||||
ownerId: null,
|
||||
};
|
||||
|
||||
describe("provider connection administrator-managed access", () => {
|
||||
beforeEach(() => {
|
||||
getProviderConnectionById.mockReset();
|
||||
getProxyPoolById.mockReset();
|
||||
updateProviderConnection.mockReset();
|
||||
deleteProviderConnection.mockReset();
|
||||
getProviderConnectionAccess.mockReset();
|
||||
});
|
||||
|
||||
it("prevents a member from updating their legacy compatible connection", async () => {
|
||||
getProviderConnectionAccess.mockResolvedValue(memberAccess);
|
||||
getProviderConnectionById.mockResolvedValue({
|
||||
id: "legacy-compatible",
|
||||
provider: "openai-compatible-chat-node",
|
||||
ownerId: "member",
|
||||
});
|
||||
|
||||
const response = await PUT(new Request("http://localhost/api/providers/legacy-compatible", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ name: "Changed" }),
|
||||
}), { params: Promise.resolve({ id: "legacy-compatible" }) });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(updateProviderConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prevents a member from deleting their legacy compatible connection", async () => {
|
||||
getProviderConnectionAccess.mockResolvedValue(memberAccess);
|
||||
getProviderConnectionById.mockResolvedValue({
|
||||
id: "legacy-compatible",
|
||||
provider: "anthropic-compatible-node",
|
||||
ownerId: "member",
|
||||
});
|
||||
|
||||
const response = await DELETE(new Request("http://localhost/api/providers/legacy-compatible", {
|
||||
method: "DELETE",
|
||||
}), { params: Promise.resolve({ id: "legacy-compatible" }) });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(deleteProviderConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows an administrator to delete a compatible connection", async () => {
|
||||
getProviderConnectionAccess.mockResolvedValue(adminAccess);
|
||||
getProviderConnectionById.mockResolvedValue({
|
||||
id: "compatible",
|
||||
provider: "custom-embedding-node",
|
||||
ownerId: "admin",
|
||||
});
|
||||
deleteProviderConnection.mockResolvedValue(true);
|
||||
|
||||
const response = await DELETE(new Request("http://localhost/api/providers/compatible", {
|
||||
method: "DELETE",
|
||||
}), { params: Promise.resolve({ id: "compatible" }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(deleteProviderConnection).toHaveBeenCalledWith("compatible");
|
||||
});
|
||||
|
||||
it("preserves member control over their non-compatible connection", async () => {
|
||||
getProviderConnectionAccess.mockResolvedValue(memberAccess);
|
||||
getProviderConnectionById.mockResolvedValue({
|
||||
id: "openai-connection",
|
||||
provider: "openai",
|
||||
ownerId: "member",
|
||||
providerSpecificData: {},
|
||||
authType: "apikey",
|
||||
});
|
||||
updateProviderConnection.mockResolvedValue({
|
||||
id: "openai-connection",
|
||||
provider: "openai",
|
||||
name: "Changed",
|
||||
});
|
||||
|
||||
const response = await PUT(new Request("http://localhost/api/providers/openai-connection", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ name: "Changed" }),
|
||||
}), { params: Promise.resolve({ id: "openai-connection" }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(updateProviderConnection).toHaveBeenCalledWith("openai-connection", {
|
||||
name: "Changed",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user