import { render, screen, waitFor, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import userEvent from "@testing-library/user-event"; import { createRoutesStub, Outlet } from "react-router"; import SecretsSettingsScreen from "#/routes/secrets-settings"; import { SecretsService } from "#/api/secrets-service"; import { GetSecretsResponse } from "#/api/secrets-service.types"; import OpenHands from "#/api/open-hands"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; const MOCK_GET_SECRETS_RESPONSE: GetSecretsResponse["custom_secrets"] = [ { name: "My_Secret_1", description: "My first secret", }, { name: "My_Secret_2", description: "My second secret", }, ]; const RouterStub = createRoutesStub([ { Component: () => , path: "/settings", children: [ { Component: SecretsSettingsScreen, path: "/settings/secrets", }, { Component: () =>
, path: "/settings/git", }, ], }, ]); const renderSecretsSettings = () => render(, { wrapper: ({ children }) => ( {children} ), }); beforeEach(() => { const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); // @ts-expect-error - only return the config we need getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); }); describe("Content", () => { it("should render the secrets settings screen", () => { renderSecretsSettings(); screen.getByTestId("secrets-settings-screen"); }); it("should NOT render a button to connect with git if they havent already in oss", async () => { const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); // @ts-expect-error - only return the config we need getConfigSpy.mockResolvedValue({ APP_MODE: "oss", }); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: {}, }); renderSecretsSettings(); expect(getConfigSpy).toHaveBeenCalled(); await waitFor(() => expect(getSecretsSpy).toHaveBeenCalled()); expect(screen.queryByTestId("connect-git-button")).not.toBeInTheDocument(); }); it("should render a button to connect with git if they havent already in saas", async () => { const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); // @ts-expect-error - only return the config we need getConfigSpy.mockResolvedValue({ APP_MODE: "saas", }); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: {}, }); renderSecretsSettings(); expect(getSecretsSpy).not.toHaveBeenCalled(); await waitFor(() => expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(), ); const button = await screen.findByTestId("connect-git-button"); await userEvent.click(button); screen.getByTestId("git-settings-screen"); }); it("should render a message if there are no existing secrets", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); getSecretsSpy.mockResolvedValue([]); renderSecretsSettings(); await screen.findByTestId("no-secrets-message"); }); it("should render existing secrets", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); renderSecretsSettings(); const secrets = await screen.findAllByTestId("secret-item"); expect(secrets).toHaveLength(2); expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument(); }); }); describe("Secret actions", () => { it("should create a new secret", async () => { const createSecretSpy = vi.spyOn(SecretsService, "createSecret"); const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); createSecretSpy.mockResolvedValue(true); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); const secrets = screen.queryAllByTestId("secret-item"); expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(); expect(secretForm).toBeInTheDocument(); expect(secrets).toHaveLength(0); // enter details const nameInput = within(secretForm).getByTestId("name-input"); const valueInput = within(secretForm).getByTestId("value-input"); const descriptionInput = within(secretForm).getByTestId("description-input"); const submitButton = within(secretForm).getByTestId("submit-button"); vi.clearAllMocks(); // reset mocks to check for upcoming calls await userEvent.type(nameInput, "My_Custom_Secret"); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.type(descriptionInput, "My custom secret description"); await userEvent.click(submitButton); // make POST request expect(createSecretSpy).toHaveBeenCalledWith( "My_Custom_Secret", "my-custom-secret-value", "My custom secret description", ); // hide form & render items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); expect(getSecretsSpy).toHaveBeenCalled(); }); it("should edit a secret", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const updateSecretSpy = vi.spyOn(SecretsService, "updateSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); updateSecretSpy.mockResolvedValue(true); renderSecretsSettings(); // render edit button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const firstSecret = within(secrets[0]); const editButton = firstSecret.getByTestId("edit-secret-button"); await userEvent.click(editButton); // render edit form const editForm = screen.getByTestId("edit-secret-form"); expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(); expect(editForm).toBeInTheDocument(); expect(screen.queryAllByTestId("secret-item")).toHaveLength(0); // enter details const nameInput = within(editForm).getByTestId("name-input"); const descriptionInput = within(editForm).getByTestId("description-input"); const submitButton = within(editForm).getByTestId("submit-button"); // should not show value input const valueInput = within(editForm).queryByTestId("value-input"); expect(valueInput).not.toBeInTheDocument(); expect(nameInput).toHaveValue("My_Secret_1"); expect(descriptionInput).toHaveValue("My first secret"); await userEvent.clear(nameInput); await userEvent.type(nameInput, "My_Edited_Secret"); await userEvent.clear(descriptionInput); await userEvent.type(descriptionInput, "My edited secret description"); await userEvent.click(submitButton); // make POST request expect(updateSecretSpy).toHaveBeenCalledWith( "My_Secret_1", "My_Edited_Secret", "My edited secret description", ); // hide form expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument(); // optimistic update const updatedSecrets = await screen.findAllByTestId("secret-item"); expect(updatedSecrets).toHaveLength(2); expect(updatedSecrets[0]).toHaveTextContent(/my_edited_secret/i); }); it("should be able to cancel the create or edit form", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); // cancel button const cancelButton = within(secretForm).getByTestId("cancel-button"); await userEvent.click(cancelButton); expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); expect(screen.queryByTestId("add-secret-button")).toBeInTheDocument(); // render edit button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const firstSecret = within(secrets[0]); const editButton = firstSecret.getByTestId("edit-secret-button"); await userEvent.click(editButton); // render edit form const editForm = screen.getByTestId("edit-secret-form"); expect(editForm).toBeInTheDocument(); expect(screen.queryAllByTestId("secret-item")).toHaveLength(0); // cancel button const cancelEditButton = within(editForm).getByTestId("cancel-button"); await userEvent.click(cancelEditButton); expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("secret-item")).toHaveLength(2); }); it("should undo the optimistic update if the request fails", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const updateSecretSpy = vi.spyOn(SecretsService, "updateSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); updateSecretSpy.mockRejectedValue(new Error("Failed to update secret")); renderSecretsSettings(); // render edit button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const firstSecret = within(secrets[0]); const editButton = firstSecret.getByTestId("edit-secret-button"); await userEvent.click(editButton); // render edit form const editForm = screen.getByTestId("edit-secret-form"); expect(editForm).toBeInTheDocument(); expect(screen.queryAllByTestId("secret-item")).toHaveLength(0); // enter details const nameInput = within(editForm).getByTestId("name-input"); const submitButton = within(editForm).getByTestId("submit-button"); // should not show value input const valueInput = within(editForm).queryByTestId("value-input"); expect(valueInput).not.toBeInTheDocument(); await userEvent.clear(nameInput); await userEvent.type(nameInput, "My_Edited_Secret"); await userEvent.click(submitButton); // make POST request expect(updateSecretSpy).toHaveBeenCalledWith( "My_Secret_1", "My_Edited_Secret", "My first secret", ); // hide form expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument(); // no optimistic update const updatedSecrets = await screen.findAllByTestId("secret-item"); expect(updatedSecrets).toHaveLength(2); expect(updatedSecrets[0]).not.toHaveTextContent(/my edited secret/i); }); it("should remove the secret from the list after deletion", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); deleteSecretSpy.mockResolvedValue(true); renderSecretsSettings(); // render delete button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const secondSecret = within(secrets[1]); const deleteButton = secondSecret.getByTestId("delete-secret-button"); await userEvent.click(deleteButton); // confirmation modal const confirmationModal = screen.getByTestId("confirmation-modal"); const confirmButton = within(confirmationModal).getByTestId("confirm-button"); await userEvent.click(confirmButton); // make DELETE request expect(deleteSecretSpy).toHaveBeenCalledWith("My_Secret_2"); expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument(); // optimistic update expect(screen.queryAllByTestId("secret-item")).toHaveLength(1); expect(screen.queryByText("My_Secret_2")).not.toBeInTheDocument(); }); it("should be able to cancel the delete confirmation modal", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); deleteSecretSpy.mockResolvedValue(true); renderSecretsSettings(); // render delete button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const secondSecret = within(secrets[1]); const deleteButton = secondSecret.getByTestId("delete-secret-button"); await userEvent.click(deleteButton); // confirmation modal const confirmationModal = screen.getByTestId("confirmation-modal"); const cancelButton = within(confirmationModal).getByTestId("cancel-button"); await userEvent.click(cancelButton); // no DELETE request expect(deleteSecretSpy).not.toHaveBeenCalled(); expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("secret-item")).toHaveLength(2); }); it("should revert the optimistic update if the request fails", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); deleteSecretSpy.mockRejectedValue(new Error("Failed to delete secret")); renderSecretsSettings(); // render delete button within a secret list item const secrets = await screen.findAllByTestId("secret-item"); const secondSecret = within(secrets[1]); const deleteButton = secondSecret.getByTestId("delete-secret-button"); await userEvent.click(deleteButton); // confirmation modal const confirmationModal = screen.getByTestId("confirmation-modal"); const confirmButton = within(confirmationModal).getByTestId("confirm-button"); await userEvent.click(confirmButton); // make DELETE request expect(deleteSecretSpy).toHaveBeenCalledWith("My_Secret_2"); expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument(); // optimistic update expect(screen.queryAllByTestId("secret-item")).toHaveLength(2); expect(screen.queryByText("My_Secret_2")).toBeInTheDocument(); }); it("should hide the no items message when in form view", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); getSecretsSpy.mockResolvedValue([]); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument(); }); it("should not allow spaces in secret names", async () => { const createSecretSpy = vi.spyOn(SecretsService, "createSecret"); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); // enter details const nameInput = within(secretForm).getByTestId("name-input"); const valueInput = within(secretForm).getByTestId("value-input"); const submitButton = within(secretForm).getByTestId("submit-button"); await userEvent.type(nameInput, "My Custom Secret With Spaces"); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.click(submitButton); // make POST request expect(createSecretSpy).not.toHaveBeenCalled(); await userEvent.clear(nameInput); await userEvent.type(nameInput, "MyCustomSecret"); await userEvent.click(submitButton); expect(createSecretSpy).toHaveBeenCalledWith( "MyCustomSecret", "my-custom-secret-value", undefined, ); }); it("should not allow existing secret names", async () => { const createSecretSpy = vi.spyOn(SecretsService, "createSecret"); const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE.slice(0, 1)); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); // enter details const nameInput = within(secretForm).getByTestId("name-input"); const valueInput = within(secretForm).getByTestId("value-input"); const submitButton = within(secretForm).getByTestId("submit-button"); await userEvent.type(nameInput, "My_Secret_1"); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.click(submitButton); // make POST request expect(createSecretSpy).not.toHaveBeenCalled(); expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument(); await userEvent.clear(nameInput); await userEvent.type(nameInput, "My_Custom_Secret"); await userEvent.clear(valueInput); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.click(submitButton); expect(createSecretSpy).toHaveBeenCalledWith( "My_Custom_Secret", "my-custom-secret-value", undefined, ); expect( screen.queryByText("SECRETS$SECRET_VALUE_REQUIRED"), ).not.toBeInTheDocument(); }); it("should not submit whitespace secret names or values", async () => { const createSecretSpy = vi.spyOn(SecretsService, "createSecret"); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); // enter details const nameInput = within(secretForm).getByTestId("name-input"); const valueInput = within(secretForm).getByTestId("value-input"); const submitButton = within(secretForm).getByTestId("submit-button"); await userEvent.type(nameInput, " "); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.click(submitButton); // make POST request expect(createSecretSpy).not.toHaveBeenCalled(); await userEvent.clear(nameInput); await userEvent.type(nameInput, "My_Custom_Secret"); await userEvent.clear(valueInput); await userEvent.type(valueInput, " "); await userEvent.click(submitButton); expect(createSecretSpy).not.toHaveBeenCalled(); expect( screen.queryByText("SECRETS$SECRET_VALUE_REQUIRED"), ).toBeInTheDocument(); }); it("should not reset ipout values on an invalid submit", async () => { const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets"); const createSecretSpy = vi.spyOn(SecretsService, "createSecret"); getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE); renderSecretsSettings(); // render form & hide items expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument(); const button = await screen.findByTestId("add-secret-button"); await userEvent.click(button); const secretForm = screen.getByTestId("add-secret-form"); expect(secretForm).toBeInTheDocument(); // enter details const nameInput = within(secretForm).getByTestId("name-input"); const valueInput = within(secretForm).getByTestId("value-input"); const submitButton = within(secretForm).getByTestId("submit-button"); await userEvent.type(nameInput, MOCK_GET_SECRETS_RESPONSE[0].name); await userEvent.type(valueInput, "my-custom-secret-value"); await userEvent.click(submitButton); // make POST request expect(createSecretSpy).not.toHaveBeenCalled(); expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument(); expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name); expect(valueInput).toHaveValue("my-custom-secret-value"); }); });