Skip to content

Commit

Permalink
chore: Git mod - Connect/Import modal (appsmithorg#38098)
Browse files Browse the repository at this point in the history
## Description
Git mod components, add connect/import from git modal components

Fixes appsmithorg#37812
Fixes appsmithorg#37802

## Automation

/ok-to-test tags="@tag.Git"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/12291098002>
> Commit: e94ebe0
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12291098002&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Git`
> Spec:
> <hr>Thu, 12 Dec 2024 07:43:05 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
  - Introduced a multi-step `ConnectModal` for Git provider connections.
- Added components for generating SSH keys and managing Git remote URLs.
- New constants for Git integration steps and demo GIFs for user
guidance.
- Added optional `errorType` property to enhance error handling in API
responses.
  - New `Steps` component for step navigation in the modal.
- New `CopyButton` component for clipboard functionality with visual
feedback.

- **Improvements**
  - Enhanced error handling and user prompts related to Git operations.
- Improved user interface with styled components for better layout and
presentation.

- **Bug Fixes**
  - Improved validation and error messaging for SSH URL inputs.

- **Refactor**
- Renamed `AutocommitStatusbar` to `Statusbar` for consistency across
components and tests.

- **Tests**
- Comprehensive test coverage for new components and functionalities
related to Git integration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
ashit-rath authored Dec 12, 2024
1 parent a040815 commit ebb341a
Show file tree
Hide file tree
Showing 19 changed files with 2,288 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/api/ApiResponses.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface APIResponseError {
code: string | number;
message: string;
errorType?: string;
}

export interface ResponseMeta {
Expand Down
4 changes: 4 additions & 0 deletions src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,10 @@ export const IS_EMPTY_REPO_QUESTION = () =>
export const HOW_TO_CREATE_EMPTY_REPO = () => "How to create a new repository?";
export const IMPORT_APP_IF_NOT_EMPTY = () =>
"If you already have an app connected to Git, you can import it to the workspace.";
export const IMPORT_ARTIFACT_IF_NOT_EMPTY = (artifactType: string) =>
`If you already have an ${artifactType.toLocaleLowerCase()} connected to Git, you can import it to the workspace.`;
export const I_HAVE_EXISTING_ARTIFACT_REPO = (artifactType: string) =>
`I have an existing appsmith ${artifactType.toLocaleLowerCase()} connected to Git`;
export const I_HAVE_EXISTING_REPO = () =>
"I have an existing appsmith app connected to Git";
export const ERROR_REPO_NOT_EMPTY_TITLE = () =>
Expand Down
270 changes: 270 additions & 0 deletions src/git/components/ConnectModal/AddDeployKey.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import type { AddDeployKeyProps } from "./AddDeployKey";
import AddDeployKey from "./AddDeployKey";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import "@testing-library/jest-dom";

jest.mock("ee/utils/AnalyticsUtil", () => ({
logEvent: jest.fn(),
}));

jest.mock("copy-to-clipboard", () => ({
__esModule: true,
default: () => true,
}));

const DEFAULT_DOCS_URL =
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/connecting-to-git-repository";

const defaultProps: AddDeployKeyProps = {
isModalOpen: true,
onChange: jest.fn(),
value: {
gitProvider: "github",
isAddedDeployKey: false,
remoteUrl: "[email protected]:owner/repo.git",
},
fetchSSHKeyPair: jest.fn(),
generateSSHKey: jest.fn(),
isFetchingSSHKeyPair: false,
isGeneratingSSHKey: false,
sshKeyPair: "ecdsa-sha2-nistp256 AAAAE2VjZHNhAAAIBaj...",
};

describe("AddDeployKey Component", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("renders without crashing and shows default UI", () => {
render(<AddDeployKey {...defaultProps} />);
expect(
screen.getByText("Add deploy key & give write access"),
).toBeInTheDocument();
expect(screen.getByRole("combobox")).toBeInTheDocument();
// Should show ECDSA by default since sshKeyPair includes "ecdsa"
expect(screen.getByText(defaultProps.sshKeyPair)).toBeInTheDocument();
expect(
screen.getByText("I've added the deploy key and gave it write access"),
).toBeInTheDocument();
});

it("calls fetchSSHKeyPair if modal is open and not importing", () => {
render(<AddDeployKey {...defaultProps} isImport={false} />);
expect(defaultProps.fetchSSHKeyPair).toHaveBeenCalledTimes(1);
});

it("does not call fetchSSHKeyPair if importing", () => {
render(<AddDeployKey {...defaultProps} isImport />);
expect(defaultProps.fetchSSHKeyPair).not.toHaveBeenCalled();
});

it("shows dummy key loader if loading keys", () => {
render(
<AddDeployKey {...defaultProps} isFetchingSSHKeyPair sshKeyPair="" />,
);
// The actual key text should not be displayed
expect(screen.queryByText("ecdsa-sha2-nistp256")).not.toBeInTheDocument();
});

it("changes SSH key type when user selects a different type and triggers generateSSHKey if needed", async () => {
const generateSSHKey = jest.fn();

render(
<AddDeployKey
{...defaultProps}
generateSSHKey={generateSSHKey}
sshKeyPair="" // No key to force generation
/>,
);

fireEvent.mouseDown(screen.getByRole("combobox"));
const rsaOption = screen.getByText("RSA 4096");

fireEvent.click(rsaOption);

await waitFor(() => {
expect(generateSSHKey).toHaveBeenCalledWith("RSA", expect.any(Object));
});
});

it("displays a generic error when errorData is provided and error code is not AE-GIT-4032 or AE-GIT-4033", () => {
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
const errorData = {
data: {},
responseMeta: {
success: false,
status: 503,
error: {
code: "GENERIC-ERROR",
errorType: "Some Error",
message: "Something went wrong",
},
},
};

render(<AddDeployKey {...defaultProps} errorData={errorData} />);
expect(screen.getByText("Some Error")).toBeInTheDocument();
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
});

it("displays a misconfiguration error if error code is AE-GIT-4032", () => {
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
const errorData = {
data: {},
responseMeta: {
success: false,
status: 503,
error: {
code: "AE-GIT-4032",
errorType: "SSH Key Error",
message: "SSH Key misconfiguration",
},
},
};

render(<AddDeployKey {...defaultProps} errorData={errorData} />);
expect(screen.getByText("SSH key misconfiguration")).toBeInTheDocument();
expect(
screen.getByText(
"It seems that your SSH key hasn't been added to your repository. To proceed, please revisit the steps below and configure your SSH key correctly.",
),
).toBeInTheDocument();
});

it("invokes onChange callback when checkbox is toggled", () => {
const onChange = jest.fn();

render(<AddDeployKey {...defaultProps} onChange={onChange} />);
const checkbox = screen.getByTestId("t--added-deploy-key-checkbox");

fireEvent.click(checkbox);
expect(onChange).toHaveBeenCalledWith({ isAddedDeployKey: true });
});

it("calls AnalyticsUtil on copy button click", () => {
render(<AddDeployKey {...defaultProps} />);
const copyButton = screen.getByTestId("t--copy-generic");

fireEvent.click(copyButton);
expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith(
"GS_COPY_SSH_KEY_BUTTON_CLICK",
);
});

it("hides copy button when connectLoading is true", () => {
render(<AddDeployKey {...defaultProps} connectLoading />);
expect(screen.queryByTestId("t--copy-generic")).not.toBeInTheDocument();
});

it("shows repository settings link if gitProvider is known and not 'others'", () => {
render(<AddDeployKey {...defaultProps} />);
const link = screen.getByRole("link", { name: "repository settings." });

expect(link).toHaveAttribute(
"href",
"https://github.com/owner/repo/settings/keys",
);
});

it("does not show repository link if gitProvider = 'others'", () => {
render(
<AddDeployKey
{...defaultProps}
value={{ gitProvider: "others", remoteUrl: "[email protected]:repo.git" }}
/>,
);
expect(
screen.queryByRole("link", { name: "repository settings." }),
).not.toBeInTheDocument();
});

it("shows collapsible section if gitProvider is not 'others'", () => {
render(
<AddDeployKey
{...defaultProps}
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
value={{
gitProvider: "gitlab",
remoteUrl: "[email protected]:owner/repo.git",
}}
/>,
);
expect(
screen.getByText("How to paste SSH Key in repo and give write access?"),
).toBeInTheDocument();
expect(screen.getByAltText("Add deploy key in gitlab")).toBeInTheDocument();
});

it("does not display collapsible if gitProvider = 'others'", () => {
render(
<AddDeployKey
{...defaultProps}
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
value={{ gitProvider: "others", remoteUrl: "[email protected]:repo.git" }}
/>,
);
expect(
screen.queryByText("How to paste SSH Key in repo and give write access?"),
).not.toBeInTheDocument();
});

it("uses default documentation link if none provided", () => {
render(<AddDeployKey {...defaultProps} />);
const docsLink = screen.getByRole("link", { name: "Read Docs" });

expect(docsLink).toHaveAttribute("href", DEFAULT_DOCS_URL);
});

it("uses custom documentation link if provided", () => {
render(
<AddDeployKey
{...defaultProps}
deployKeyDocUrl="https://custom-docs.com"
/>,
);
const docsLink = screen.getByRole("link", { name: "Read Docs" });

expect(docsLink).toHaveAttribute("href", "https://custom-docs.com");
});

it("does not generate SSH key if modal is closed", () => {
const generateSSHKey = jest.fn();

render(
<AddDeployKey
{...defaultProps}
generateSSHKey={generateSSHKey}
isModalOpen={false}
sshKeyPair=""
/>,
);
// Should not call generateSSHKey since modal is not open
expect(generateSSHKey).not.toHaveBeenCalled();
});

it("generates SSH key if none is present and conditions are met", async () => {
const fetchSSHKeyPair = jest.fn((props) => {
props.onSuccessCallback && props.onSuccessCallback();
});
const generateSSHKey = jest.fn();

render(
<AddDeployKey
{...defaultProps}
fetchSSHKeyPair={fetchSSHKeyPair}
generateSSHKey={generateSSHKey}
isFetchingSSHKeyPair={false}
isGeneratingSSHKey={false}
sshKeyPair=""
/>,
);

expect(fetchSSHKeyPair).toHaveBeenCalledTimes(1);

await waitFor(() => {
expect(generateSSHKey).toHaveBeenCalledWith("ECDSA", expect.any(Object));
});
});
});
Loading

0 comments on commit ebb341a

Please sign in to comment.