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:
yuneng-jiang 2026-02-18 18:11:07 -08:00
parent 876c4cfd0f
commit f1cbaac124
2 changed files with 453 additions and 0 deletions

View file

@ -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");
});
});

View file

@ -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();
});
});
});