import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { renderToString } from "react-dom/server"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from "vitest"; import { updateSamlConfig } from "@/actions/integrations"; import { RUNTIME_CONFIG_SCRIPT_ID, type RuntimePublicConfig, } from "@/lib/runtime-config.shared"; import { useCloudUpgradeStore } from "@/store"; import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade"; import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS, SAML_CONFIGURATION_RESOURCE_TYPE, } from "@/types/saml"; import { SamlConfigForm } from "./saml-config-form"; const { isCloudMock, updateSamlConfigMock } = vi.hoisted(() => ({ isCloudMock: vi.fn(), updateSamlConfigMock: vi.fn(), })); vi.mock("@/actions/integrations", () => ({ createSamlConfig: vi.fn(), updateSamlConfig: updateSamlConfigMock, })); vi.mock("@/components/shadcn", async (importOriginal) => ({ ...(await importOriginal>()), useToast: () => ({ toast: vi.fn() }), })); vi.mock("@/lib/shared/env", () => ({ isCloud: isCloudMock, })); const runtimeConfig: RuntimePublicConfig = { sentryDsn: null, sentryEnvironment: null, googleTagManagerId: null, apiBaseUrl: "https://api.example.com/api/v1", apiDocsUrl: null, posthogEnabled: false, posthogKey: null, posthogHost: null, reoDevClientId: null, cloudEnabled: false, cloudBillingEnabled: false, stripePublishableKey: null, stripePublishableKeyV2: null, }; const capturePendingFileReads = () => { const readers: FileReader[] = []; vi.spyOn(FileReader.prototype, "readAsText").mockImplementation(function ( this: FileReader, ) { readers.push(this); }); vi.spyOn(FileReader.prototype, "abort").mockImplementation(() => {}); return readers; }; const finishFileRead = (reader: FileReader, content: string) => { Object.defineProperty(reader, "result", { configurable: true, value: content, }); reader.dispatchEvent(new ProgressEvent("load")); }; const renderSamlUpdateForm = () => { isCloudMock.mockReturnValue(true); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", metadata_xml: "", }, }; render(); return { fileInput: document.getElementById("metadata_xml_file") as HTMLInputElement, metadataInput: document.getElementById("metadata_xml") as HTMLInputElement, }; }; beforeAll(() => { const runtimeConfigScript = document.createElement("script"); runtimeConfigScript.id = RUNTIME_CONFIG_SCRIPT_ID; runtimeConfigScript.type = "application/json"; runtimeConfigScript.textContent = JSON.stringify(runtimeConfig); document.head.append(runtimeConfigScript); }); beforeEach(() => { isCloudMock.mockReturnValue(false); vi.mocked(updateSamlConfig).mockReset(); useCloudUpgradeStore.getState().closeCloudUpgrade(); }); afterEach(() => { vi.restoreAllMocks(); }); afterAll(() => { document.getElementById(RUNTIME_CONFIG_SCRIPT_ID)?.remove(); }); describe("SamlConfigForm", () => { it("keeps the ACS field container while generating the URL", async () => { // Given const serverHtml = renderToString(); const user = userEvent.setup(); const container = document.body.appendChild(document.createElement("div")); container.innerHTML = serverHtml; render(, { container, hydrate: true, }); const acsGuidance = screen.getByText( "Enter your email domain above to generate the ACS URL.", ); const acsField = acsGuidance.parentElement; expect(acsField).toHaveClass("h-10", "w-full"); expect( screen.queryByRole("button", { name: "Copy ACS URL" }), ).not.toBeInTheDocument(); // When await user.type( screen.getByLabelText("Primary Email Domain*"), "example.com", ); // Then const acsUrl = screen.getByText( "https://api.example.com/api/v1/accounts/saml/example.com/acs/", ); expect(acsUrl.parentElement).toBe(acsField); expect(screen.getByRole("button", { name: "Copy ACS URL" })).toBeVisible(); }); it("normalizes whitespace and mixed case in the ACS URL", async () => { // Given const user = userEvent.setup(); render(); // When await user.type( screen.getByLabelText("Primary Email Domain*"), " Example.COM ", ); // Then expect( screen.getByText( "https://api.example.com/api/v1/accounts/saml/example.com/acs/", ), ).toBeVisible(); }); it("keeps single-domain SAML available in OSS and opens the Cloud upgrade for aliases", async () => { // Given const user = userEvent.setup(); render(); // When const cloudUpgradeButton = screen.getByRole("button", { name: "Additional email domains are available in Prowler Cloud", }); await user.click(cloudUpgradeButton); // Then expect(screen.getByLabelText("Primary Email Domain*")).toBeEnabled(); expect( screen.queryByLabelText("Additional Email Domain"), ).not.toBeInTheDocument(); expect(cloudUpgradeButton).toHaveClass("border-0", "bg-transparent", "p-0"); expect( screen.getByText("Additional Email Domains").parentElement, ).toContainElement(cloudUpgradeButton); expect(screen.getByText("Available in Prowler Cloud")).toBeVisible(); expect(useCloudUpgradeStore.getState().activeFeature).toBe( CLOUD_UPGRADE_FEATURE.SAML_DOMAINS, ); }); it("loads existing aliases in Cloud and keeps the canonical ACS", () => { // Given isCloudMock.mockReturnValue(true); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", additional_email_domains: [ "subsidiary.example.com", "division.example.com", ], metadata_xml: "", }, }; // When render(); // Then expect(screen.getByText("subsidiary.example.com")).toBeVisible(); expect(screen.getByText("division.example.com")).toBeVisible(); expect( screen.getByText( "https://api.example.com/api/v1/accounts/saml/primary.example.com/acs/", ), ).toBeVisible(); expect( screen.queryByText(/accounts\/saml\/subsidiary\.example\.com\/acs/), ).not.toBeInTheDocument(); }); it("does not mark metadata XML as required when updating", () => { // Given - An existing SAML configuration // When renderSamlUpdateForm(); // Then expect(screen.getByText("Metadata XML File")).not.toHaveTextContent("*"); }); it("does not require metadata when an update file selection is cleared", async () => { // Given const user = userEvent.setup(); const { fileInput, metadataInput } = renderSamlUpdateForm(); const metadataFile = new File([""], "metadata.xml", { type: "application/xml", }); await user.upload(fileInput, metadataFile); await waitFor(() => expect(metadataInput).toHaveValue(""), ); // When await user.upload(fileInput, []); // Then expect(metadataInput).toHaveValue(""); expect( screen.queryByText("Metadata XML is required"), ).not.toBeInTheDocument(); }); it("ignores a discarded metadata file when its read finishes", async () => { // Given const readers = capturePendingFileReads(); const user = userEvent.setup(); const { fileInput, metadataInput } = renderSamlUpdateForm(); const discardedFile = new File( [''], "discarded.xml", { type: "application/xml" }, ); await user.upload(fileInput, discardedFile); expect(readers).toHaveLength(1); // When await user.upload(fileInput, []); act(() => { finishFileRead(readers[0], ''); }); // Then expect(metadataInput).toHaveValue(""); expect(screen.queryByText("discarded.xml")).not.toBeInTheDocument(); }); it("keeps the latest metadata file when an earlier read finishes last", async () => { // Given const readers = capturePendingFileReads(); const user = userEvent.setup(); const { fileInput, metadataInput } = renderSamlUpdateForm(); const earlierFile = new File( [''], "earlier.xml", { type: "application/xml" }, ); const latestFile = new File( [''], "latest.xml", { type: "application/xml" }, ); await user.upload(fileInput, earlierFile); await user.upload(fileInput, latestFile); expect(readers).toHaveLength(2); // When act(() => { finishFileRead(readers[1], ''); finishFileRead(readers[0], ''); }); // Then expect(metadataInput).toHaveValue(''); expect(screen.getByText("latest.xml")).toBeVisible(); expect(screen.queryByText("earlier.xml")).not.toBeInTheDocument(); }); it("prevents updates while the metadata file is being read", async () => { // Given const readers = capturePendingFileReads(); const user = userEvent.setup(); const { fileInput } = renderSamlUpdateForm(); const updateButton = screen.getByRole("button", { name: "Update" }); const metadataFile = new File( [''], "replacement.xml", { type: "application/xml" }, ); // When await user.upload(fileInput, metadataFile); // Then expect(readers).toHaveLength(1); expect(updateButton).toBeDisabled(); // When act(() => { finishFileRead(readers[0], ''); }); // Then expect(updateButton).toBeEnabled(); }); it("marks metadata XML as required when creating", () => { // Given isCloudMock.mockReturnValue(true); // When render(); // Then expect(screen.getByText("Metadata XML File")).toHaveTextContent("*"); }); it("adds normalized aliases and removes them in Cloud", async () => { // Given isCloudMock.mockReturnValue(true); const user = userEvent.setup(); render(); // When await user.type( screen.getByLabelText("Additional Email Domain"), " Subsidiary.Example.com ", ); await user.click(screen.getByRole("button", { name: "Add domain" })); // Then expect(screen.getByText("subsidiary.example.com")).toBeVisible(); expect( document.querySelectorAll( 'input[name="additional_email_domains"][value="subsidiary.example.com"]', ), ).toHaveLength(1); // When await user.click( screen.getByRole("button", { name: "Remove subsidiary.example.com", }), ); // Then expect( screen.queryByText("subsidiary.example.com"), ).not.toBeInTheDocument(); expect( document.querySelectorAll('input[name="additional_email_domains"]'), ).toHaveLength(0); }); it("includes a pending additional domain in the submitted form data", async () => { // Given const user = userEvent.setup(); const { fileInput } = renderSamlUpdateForm(); const form = fileInput.form; expect(form).not.toBeNull(); // When await user.type( screen.getByLabelText("Additional Email Domain"), " Pending.Example.com ", ); // Then expect(new FormData(form!).getAll("additional_email_domains")).toEqual([ " Pending.Example.com ", ]); }); it("submits a normalized pending domain through the update action", async () => { // Given isCloudMock.mockReturnValue(true); vi.mocked(updateSamlConfig).mockResolvedValue({ success: "SAML configuration updated successfully!", }); const setIsOpen = vi.fn(); const user = userEvent.setup(); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", additional_email_domains: ["existing.example.com"], metadata_xml: "", }, }; render(); await user.type( screen.getByLabelText("Additional Email Domain"), " Pending.Example.com ", ); // When await user.click(screen.getByRole("button", { name: "Update" })); // Then await waitFor(() => expect(updateSamlConfig).toHaveBeenCalledOnce()); const submittedFormData = vi.mocked(updateSamlConfig).mock.calls[0][1]; expect(submittedFormData.get("email_domain")).toBe("primary.example.com"); expect(submittedFormData.getAll("additional_email_domains")).toEqual([ "existing.example.com", "pending.example.com", ]); expect(setIsOpen).toHaveBeenCalledWith(false); }); it("shows an additional-domain error returned by the update action", async () => { // Given isCloudMock.mockReturnValue(true); vi.mocked(updateSamlConfig).mockResolvedValue({ errors: { additional_email_domains: "Domain is already in use.", }, }); const user = userEvent.setup(); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", additional_email_domains: ["alias.example.com"], metadata_xml: "", }, }; render(); // When await user.click(screen.getByRole("button", { name: "Update" })); // Then expect(await screen.findByText("Domain is already in use.")).toBeVisible(); }); it("shows a contextual error when an additional domain is required", async () => { // Given isCloudMock.mockReturnValue(true); const user = userEvent.setup(); render(); // When await user.click(screen.getByRole("button", { name: "Add domain" })); // Then expect( screen.getByText("Additional email domain is required"), ).toBeVisible(); }); it("keeps the maximum alias set inside a compact scroll region", () => { // Given isCloudMock.mockReturnValue(true); const additionalEmailDomains = Array.from( { length: MAX_SAML_ADDITIONAL_EMAIL_DOMAINS }, (_, index) => `alias-${index + 1}.example.com`, ); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", additional_email_domains: additionalEmailDomains, metadata_xml: "", }, }; // When render(); // Then expect( screen.getByText( `${MAX_SAML_ADDITIONAL_EMAIL_DOMAINS} / ${MAX_SAML_ADDITIONAL_EMAIL_DOMAINS} domains`, ), ).toBeVisible(); expect( screen.getByRole("list", { name: "Additional email domains" }), ).toHaveClass("max-h-24", "overflow-y-auto"); expect(screen.getByLabelText("Additional Email Domain")).toBeDisabled(); expect(screen.getByRole("button", { name: "Add domain" })).toBeDisabled(); expect( screen.getAllByRole("button", { name: /^Remove alias-/ }), ).toHaveLength(MAX_SAML_ADDITIONAL_EMAIL_DOMAINS); }); it("rejects a duplicate alias before submission", async () => { // Given isCloudMock.mockReturnValue(true); const user = userEvent.setup(); const samlConfig = { type: SAML_CONFIGURATION_RESOURCE_TYPE, id: "saml-1", attributes: { email_domain: "primary.example.com", additional_email_domains: ["alias.example.com"], metadata_xml: "", }, }; render(); // When await user.type( screen.getByLabelText("Additional Email Domain"), " ALIAS.EXAMPLE.COM ", ); await user.click(screen.getByRole("button", { name: "Add domain" })); // Then expect( screen.getByText("This domain has already been added."), ).toBeVisible(); expect(screen.getAllByText("alias.example.com")).toHaveLength(1); }); });