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();
+ },
},
]}
>