mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Add unit tests for ModelSettingsModal and useStoreModelInDB hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
876c4cfd0f
commit
f1cbaac124
2 changed files with 453 additions and 0 deletions
|
|
@ -0,0 +1,147 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useStoreModelInDB } from "./useStoreModelInDB";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
|
||||
}));
|
||||
|
||||
describe("useStoreModelInDB", () => {
|
||||
let queryClient: QueryClient;
|
||||
let fetchSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = vi.fn();
|
||||
global.fetch = fetchSpy;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
|
||||
it("should send correct request body to /config/field/update", async () => {
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ message: "Success" }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
|
||||
|
||||
result.current.mutate({ store_model_in_db: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"/config/field/update",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
field_name: "store_model_in_db",
|
||||
field_value: true,
|
||||
config_type: "general_settings",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle setting store_model_in_db to false", async () => {
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ message: "Success" }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
|
||||
|
||||
result.current.mutate({ store_model_in_db: false });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"/config/field/update",
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
field_name: "store_model_in_db",
|
||||
field_value: false,
|
||||
config_type: "general_settings",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error when access token is missing", async () => {
|
||||
vi.spyOn(
|
||||
await import("../useAuthorized"),
|
||||
"default"
|
||||
).mockReturnValue({
|
||||
accessToken: null,
|
||||
userRole: null,
|
||||
userId: null,
|
||||
token: null,
|
||||
userEmail: null,
|
||||
premiumUser: false,
|
||||
disabledPersonalKeyCreation: null,
|
||||
showSSOBanner: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
|
||||
|
||||
result.current.mutate({ store_model_in_db: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error?.message).toBe("Access token is required");
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle API error response", async () => {
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: false,
|
||||
json: async () => ({ detail: "Unauthorized" }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
|
||||
|
||||
result.current.mutate({ store_model_in_db: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error?.message).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should use fallback error message when API returns empty error", async () => {
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: false,
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
|
||||
|
||||
result.current.mutate({ store_model_in_db: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error?.message).toBe("Failed to update model storage settings");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
import { useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
|
||||
import { useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { parseErrorMessage } from "@/components/shared/errorUtils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../../tests/test-utils";
|
||||
import ModelSettingsModal from "./ModelSettingsModal";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB");
|
||||
vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig");
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/components/shared/errorUtils", () => ({
|
||||
parseErrorMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseStoreModelInDB = vi.mocked(useStoreModelInDB);
|
||||
const mockUseProxyConfig = vi.mocked(useProxyConfig);
|
||||
const mockNotificationsManager = vi.mocked(NotificationsManager);
|
||||
const mockParseErrorMessage = vi.mocked(parseErrorMessage);
|
||||
|
||||
describe("ModelSettingsModal", () => {
|
||||
const mockOnCancel = vi.fn();
|
||||
const mockOnSuccess = vi.fn();
|
||||
const mockMutateAsync = vi.fn();
|
||||
const mockRefetch = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
isVisible: true,
|
||||
onCancel: mockOnCancel,
|
||||
onSuccess: mockOnSuccess,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseStoreModelInDB.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
mockUseProxyConfig.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
mockParseErrorMessage.mockImplementation((error: any) => error?.message || String(error));
|
||||
});
|
||||
|
||||
it("should render the modal", () => {
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render form field with initial values", () => {
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
expect(screen.getByText("Store Model in DB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render cancel and save buttons", () => {
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Save Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onCancel when cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onCancel when modal close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: /close/i });
|
||||
await user.click(closeButton);
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should toggle store model switch", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
expect(switchElement).not.toBeChecked();
|
||||
|
||||
await user.click(switchElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should submit form with store_model_in_db enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await user.click(switchElement);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{ store_model_in_db: true },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should submit form with store_model_in_db disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{ store_model_in_db: false },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show success notification and call onSuccess on successful submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Model storage settings updated successfully");
|
||||
expect(mockRefetch).toHaveBeenCalled();
|
||||
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification when submission fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Network error");
|
||||
mockMutateAsync.mockRejectedValue(error);
|
||||
mockParseErrorMessage.mockReturnValue("Network error");
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save model storage settings: Network error");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification from onError callback", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Backend error");
|
||||
mockMutateAsync.mockImplementation((params, options) => {
|
||||
options?.onError?.(error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
mockParseErrorMessage.mockReturnValue("Backend error");
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save model storage settings: Backend error");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable cancel button when pending", () => {
|
||||
mockUseStoreModelInDB.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable cancel button when loading config", () => {
|
||||
mockUseProxyConfig.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should show loading state on save button when pending", () => {
|
||||
mockUseStoreModelInDB.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /Saving/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
expect(saveButton.className).toContain("ant-btn-loading");
|
||||
});
|
||||
|
||||
it("should not render modal when isVisible is false", () => {
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} isVisible={false} />);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call refetch when modal opens", () => {
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
expect(mockRefetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should render form with initial values from config data", () => {
|
||||
mockUseProxyConfig.mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
field_name: "store_model_in_db",
|
||||
field_type: "bool",
|
||||
field_description: "Store model in DB",
|
||||
field_value: true,
|
||||
stored_in_db: true,
|
||||
field_default_value: false,
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
|
||||
it("should show skeleton loader when config is loading", () => {
|
||||
mockUseProxyConfig.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
|
||||
const skeletons = document.querySelectorAll(".ant-skeleton");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not call onSuccess when it is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutateAsync.mockImplementation(async (params, options) => {
|
||||
await Promise.resolve();
|
||||
options?.onSuccess?.();
|
||||
return { message: "Success" };
|
||||
});
|
||||
|
||||
renderWithProviders(<ModelSettingsModal isVisible={true} onCancel={mockOnCancel} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Settings" });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue