diff --git a/ui/litellm-dashboard/src/components/SSOModals.test.tsx b/ui/litellm-dashboard/src/components/SSOModals.test.tsx index 002672d477..59770dafbd 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.test.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.test.tsx @@ -96,7 +96,7 @@ describe("SSOModals", () => { }); }); - it("should automatically remove trailing slash from the proxy base url", async () => { + it("should show validation error if the proxy base url ends with a trailing slash", async () => { const TestWrapper = () => { const [form] = Form.useForm(); return ( @@ -115,18 +115,137 @@ describe("SSOModals", () => { ); }; - const { getByLabelText, container } = render(); + const { getByLabelText, getByText, container } = render(); - // Fill in the proxy base url with a trailing slash + // Find and interact with the SSO provider select + const ssoProviderSelect = container.querySelector("#sso_provider"); + if (ssoProviderSelect) { + fireEvent.mouseDown(ssoProviderSelect); + // Wait for dropdown and select Google + await waitFor(() => { + const googleOption = getByText("Google SSO"); + fireEvent.click(googleOption); + }); + } + + // Fill in the email field + const emailInput = getByLabelText("Proxy Admin Email"); + fireEvent.change(emailInput, { target: { value: "test@example.com" } }); + + // Fill in a URL with trailing slash const urlInput = getByLabelText("PROXY BASE URL") as HTMLInputElement; fireEvent.change(urlInput, { target: { value: "https://example.com/" } }); - // Trigger blur to ensure normalization is applied - fireEvent.blur(urlInput); + // Submit the form + const saveButton = getByText("Save"); + fireEvent.click(saveButton); - // Check that the trailing slash was removed by the normalize function + // Check for validation error await waitFor(() => { - expect(urlInput.value).toBe("https://example.com"); + expect(getByText("URL must not end with a trailing slash")).toBeInTheDocument(); }); }); + + it("should allow typing https:// without interfering with slashes", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + return ( + {}} + handleAddSSOCancel={() => {}} + handleShowInstructions={() => {}} + handleInstructionsOk={() => {}} + handleInstructionsCancel={() => {}} + form={form} + accessToken={null} + ssoConfigured={false} + /> + ); + }; + + const { getByLabelText } = render(); + + const urlInput = getByLabelText("PROXY BASE URL") as HTMLInputElement; + + // Simulate user typing "https://" + fireEvent.change(urlInput, { target: { value: "h" } }); + expect(urlInput.value).toBe("h"); + + fireEvent.change(urlInput, { target: { value: "ht" } }); + expect(urlInput.value).toBe("ht"); + + fireEvent.change(urlInput, { target: { value: "http" } }); + expect(urlInput.value).toBe("http"); + + fireEvent.change(urlInput, { target: { value: "https" } }); + expect(urlInput.value).toBe("https"); + + fireEvent.change(urlInput, { target: { value: "https:" } }); + expect(urlInput.value).toBe("https:"); + + fireEvent.change(urlInput, { target: { value: "https:/" } }); + expect(urlInput.value).toBe("https:/"); + + fireEvent.change(urlInput, { target: { value: "https://" } }); + expect(urlInput.value).toBe("https://"); + + // Continue typing the domain + fireEvent.change(urlInput, { target: { value: "https://example.com" } }); + expect(urlInput.value).toBe("https://example.com"); + }); + + it("should only show URL format error for incomplete URLs, not trailing slash error", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + return ( + {}} + handleAddSSOCancel={() => {}} + handleShowInstructions={() => {}} + handleInstructionsOk={() => {}} + handleInstructionsCancel={() => {}} + form={form} + accessToken={null} + ssoConfigured={false} + /> + ); + }; + + const { getByLabelText, getByText, queryByText, container } = render(); + + // Find and interact with the SSO provider select + const ssoProviderSelect = container.querySelector("#sso_provider"); + if (ssoProviderSelect) { + fireEvent.mouseDown(ssoProviderSelect); + // Wait for dropdown and select Google + await waitFor(() => { + const googleOption = getByText("Google SSO"); + fireEvent.click(googleOption); + }); + } + + // Fill in the email field + const emailInput = getByLabelText("Proxy Admin Email"); + fireEvent.change(emailInput, { target: { value: "test@example.com" } }); + + // Fill in an incomplete URL like "http:" + const urlInput = getByLabelText("PROXY BASE URL"); + fireEvent.change(urlInput, { target: { value: "http:" } }); + + // Submit the form + const saveButton = getByText("Save"); + fireEvent.click(saveButton); + + // Check that only the URL format error appears + await waitFor(() => { + expect(getByText("URL must start with http:// or https://")).toBeInTheDocument(); + }); + + // Verify the trailing slash error does NOT appear + expect(queryByText("URL must not end with a trailing slash")).not.toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 97b015f63b..2a9644047a 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -309,7 +309,7 @@ const SSOModals: React.FC = ({ value?.trim().replace(/\/+$/, "")} + normalize={(value) => value?.trim()} rules={[ { required: true, message: "Please enter the proxy base url" }, { @@ -317,8 +317,13 @@ const SSOModals: React.FC = ({ message: "URL must start with http:// or https://", }, { - pattern: /^https?:\/\/[^\s]+[^\/]$/, - message: "URL must not end with a trailing slash", + validator: (_, value) => { + // Only check for trailing slash if the URL starts with http:// or https:// + if (value && /^https?:\/\/.+/.test(value) && value.endsWith("/")) { + return Promise.reject("URL must not end with a trailing slash"); + } + return Promise.resolve(); + }, }, ]} >