diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx
new file mode 100644
index 0000000000..f6fcfb2fb7
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx
@@ -0,0 +1,383 @@
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { renderWithProviders, screen } from "../../../tests/test-utils";
+import KeyLifecycleSettings from "./KeyLifecycleSettings";
+
+vi.mock("antd", () => {
+ const Option = ({ children, value }: any) => (
+
+ );
+ const Select = ({ children, value, onChange, placeholder }: any) => (
+
+ );
+ Select.Option = Option;
+ return {
+ Select,
+ Tooltip: ({ children, title }: any) => (
+
+ {children}
+
+ ),
+ Switch: ({ checked, onChange }: any) => (
+ onChange(e.target.checked)}
+ />
+ ),
+ Divider: () =>
,
+ };
+});
+
+vi.mock("@ant-design/icons", () => ({
+ InfoCircleOutlined: () => ℹ,
+}));
+
+vi.mock("@tremor/react", () => ({
+ TextInput: ({ value, onValueChange, onChange, placeholder, name, className }: any) => {
+ const handleChange = (e: React.ChangeEvent) => {
+ if (onChange) {
+ onChange(e);
+ }
+ if (onValueChange) {
+ onValueChange(e.target.value);
+ }
+ };
+ return (
+
+ );
+ },
+}));
+
+describe("KeyLifecycleSettings", () => {
+ const mockForm = {
+ getFieldValue: vi.fn(),
+ setFieldValue: vi.fn(),
+ setFieldsValue: vi.fn(),
+ };
+
+ const defaultProps = {
+ form: mockForm,
+ autoRotationEnabled: false,
+ onAutoRotationChange: vi.fn(),
+ rotationInterval: "",
+ onRotationIntervalChange: vi.fn(),
+ isCreateMode: false,
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockForm.getFieldValue.mockReturnValue("");
+ });
+
+ it("should render without crashing", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Key Expiry Settings")).toBeInTheDocument();
+ expect(screen.getByText("Auto-Rotation Settings")).toBeInTheDocument();
+ });
+
+ describe("Key Expiry Settings", () => {
+ it("should render expiry input field", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Expire Key")).toBeInTheDocument();
+ expect(screen.getByTestId("duration-input")).toBeInTheDocument();
+ });
+
+ it("should show correct placeholder in create mode", () => {
+ renderWithProviders();
+
+ const input = screen.getByTestId("duration-input");
+ expect(input).toHaveAttribute(
+ "placeholder",
+ "e.g., 30d or leave empty to never expire"
+ );
+ });
+
+ it("should show correct placeholder in edit mode", () => {
+ renderWithProviders();
+
+ const input = screen.getByTestId("duration-input");
+ expect(input).toHaveAttribute("placeholder", "e.g., 30d or -1 to never expire");
+ });
+
+ it("should show correct tooltip in create mode", () => {
+ renderWithProviders();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ const expiryTooltip = tooltips.find((tooltip) =>
+ tooltip.getAttribute("title")?.includes("Leave empty to never expire")
+ );
+ expect(expiryTooltip).toBeInTheDocument();
+ expect(expiryTooltip).toHaveAttribute(
+ "title",
+ "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to never expire."
+ );
+ });
+
+ it("should show correct tooltip in edit mode", () => {
+ renderWithProviders();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ const expiryTooltip = tooltips.find((tooltip) =>
+ tooltip.getAttribute("title")?.includes("Use -1 to never expire")
+ );
+ expect(expiryTooltip).toBeInTheDocument();
+ expect(expiryTooltip).toHaveAttribute(
+ "title",
+ "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Use -1 to never expire."
+ );
+ });
+
+ it("should initialize with form value if present", () => {
+ mockForm.getFieldValue.mockReturnValue("30d");
+ renderWithProviders();
+
+ const input = screen.getByTestId("duration-input") as HTMLInputElement;
+ expect(input.value).toBe("30d");
+ });
+
+ it("should update form using setFieldValue when duration changes", async () => {
+ const user = userEvent.setup();
+ renderWithProviders();
+
+ const input = screen.getByTestId("duration-input");
+ await user.type(input, "60d");
+
+ expect(mockForm.setFieldValue).toHaveBeenCalledWith("duration", "60d");
+ });
+
+ it("should update form using setFieldsValue when setFieldValue is not available", async () => {
+ const user = userEvent.setup();
+ const formWithoutSetFieldValue = {
+ getFieldValue: vi.fn().mockReturnValue(""),
+ setFieldsValue: vi.fn(),
+ };
+ renderWithProviders(
+
+ );
+
+ const input = screen.getByTestId("duration-input");
+ await user.type(input, "90d");
+
+ expect(formWithoutSetFieldValue.setFieldsValue).toHaveBeenCalledWith({ duration: "90d" });
+ });
+ });
+
+ describe("Auto-Rotation Settings", () => {
+ it("should render auto-rotation switch", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Enable Auto-Rotation")).toBeInTheDocument();
+ expect(screen.getByTestId("switch")).toBeInTheDocument();
+ });
+
+ it("should show switch as unchecked when autoRotationEnabled is false", () => {
+ renderWithProviders();
+
+ const switchElement = screen.getByTestId("switch") as HTMLInputElement;
+ expect(switchElement.checked).toBe(false);
+ });
+
+ it("should show switch as checked when autoRotationEnabled is true", () => {
+ renderWithProviders();
+
+ const switchElement = screen.getByTestId("switch") as HTMLInputElement;
+ expect(switchElement.checked).toBe(true);
+ });
+
+ it("should call onAutoRotationChange when switch is toggled", async () => {
+ const user = userEvent.setup();
+ const onAutoRotationChange = vi.fn();
+ renderWithProviders(
+
+ );
+
+ const switchElement = screen.getByTestId("switch");
+ await user.click(switchElement);
+
+ expect(onAutoRotationChange).toHaveBeenCalledWith(true);
+ });
+
+ it("should not show rotation interval section when auto-rotation is disabled", () => {
+ renderWithProviders();
+
+ expect(screen.queryByText("Rotation Interval")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("select")).not.toBeInTheDocument();
+ });
+
+ it("should show rotation interval section when auto-rotation is enabled", () => {
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText("Rotation Interval")).toBeInTheDocument();
+ expect(screen.getByTestId("select")).toBeInTheDocument();
+ });
+
+ it("should show all predefined interval options", () => {
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText("7 days")).toBeInTheDocument();
+ expect(screen.getByText("30 days")).toBeInTheDocument();
+ expect(screen.getByText("90 days")).toBeInTheDocument();
+ expect(screen.getByText("180 days")).toBeInTheDocument();
+ expect(screen.getByText("365 days")).toBeInTheDocument();
+ expect(screen.getByText("Custom interval")).toBeInTheDocument();
+ });
+
+ it("should display current rotation interval in select", () => {
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select") as HTMLSelectElement;
+ expect(select.value).toBe("90d");
+ });
+
+ it("should call onRotationIntervalChange when predefined interval is selected", async () => {
+ const user = userEvent.setup();
+ const onRotationIntervalChange = vi.fn();
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select");
+ await user.selectOptions(select, "30d");
+
+ expect(onRotationIntervalChange).toHaveBeenCalledWith("30d");
+ });
+
+ it("should show custom input when custom option is selected", async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select");
+ await user.selectOptions(select, "custom");
+
+ expect(screen.getByTestId("custom-interval-input")).toBeInTheDocument();
+ expect(screen.getByText("Supported formats: seconds (s), minutes (m), hours (h), days (d)")).toBeInTheDocument();
+ });
+
+ it("should hide custom input when predefined interval is selected after custom", async () => {
+ const user = userEvent.setup();
+ const onRotationIntervalChange = vi.fn();
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select");
+ await user.selectOptions(select, "7d");
+
+ expect(screen.queryByTestId("custom-interval-input")).not.toBeInTheDocument();
+ expect(onRotationIntervalChange).toHaveBeenCalledWith("7d");
+ });
+
+ it("should call onRotationIntervalChange when custom interval is entered", async () => {
+ const user = userEvent.setup();
+ const onRotationIntervalChange = vi.fn();
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select");
+ await user.selectOptions(select, "custom");
+
+ const customInput = screen.getByTestId("custom-interval-input");
+ await user.type(customInput, "14d");
+
+ expect(onRotationIntervalChange).toHaveBeenCalledWith("14d");
+ });
+
+ it("should show info message when auto-rotation is enabled", () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByText(
+ "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period."
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("should not show info message when auto-rotation is disabled", () => {
+ renderWithProviders();
+
+ expect(
+ screen.queryByText(
+ "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period."
+ )
+ ).not.toBeInTheDocument();
+ });
+
+ it("should initialize with custom interval input visible when custom interval is provided", () => {
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByTestId("custom-interval-input")).toBeInTheDocument();
+ const customInput = screen.getByTestId("custom-interval-input") as HTMLInputElement;
+ expect(customInput.value).toBe("14d");
+ });
+
+ it("should show custom option selected when custom interval is provided", () => {
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select") as HTMLSelectElement;
+ expect(select.value).toBe("custom");
+ });
+
+ it("should not call onRotationIntervalChange when selecting custom option", async () => {
+ const user = userEvent.setup();
+ const onRotationIntervalChange = vi.fn();
+ renderWithProviders(
+
+ );
+
+ const select = screen.getByTestId("select");
+ await user.selectOptions(select, "custom");
+
+ expect(onRotationIntervalChange).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
index 81d22b5634..0f29a47d1d 100644
--- a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
@@ -11,6 +11,7 @@ interface KeyLifecycleSettingsProps {
onAutoRotationChange: (enabled: boolean) => void;
rotationInterval: string;
onRotationIntervalChange: (interval: string) => void;
+ isCreateMode?: boolean; // If true, shows "leave empty to never expire" instead of "-1 to never expire"
}
const KeyLifecycleSettings: React.FC = ({
@@ -19,6 +20,7 @@ const KeyLifecycleSettings: React.FC = ({
onAutoRotationChange,
rotationInterval,
onRotationIntervalChange,
+ isCreateMode = false,
}) => {
// Predefined intervals
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
@@ -64,13 +66,19 @@ const KeyLifecycleSettings: React.FC = ({
= ({ team, teams, data, addKey }) => {
formValues.rotation_interval = rotationInterval;
}
- // Handle duration field for key expiry
- if (formValues.duration) {
- formValues.duration = formValues.duration;
+ // Handle duration field for key expiry - convert empty string to null
+ if (!formValues.duration || formValues.duration.trim() === "") {
+ formValues.duration = null;
}
// Update the formValues with the final metadata
@@ -1270,6 +1270,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => {
onAutoRotationChange={setAutoRotationEnabled}
rotationInterval={rotationInterval}
onRotationIntervalChange={setRotationInterval}
+ isCreateMode={true}
/>