Skip to content

Commit

Permalink
DEVPROD-9282: Add button to delete GitHub app credentials (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
minnakt authored Aug 1, 2024
1 parent ad80526 commit 0544693
Show file tree
Hide file tree
Showing 15 changed files with 295 additions and 73 deletions.
1 change: 1 addition & 0 deletions apps/parsley/src/gql/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,7 @@ export type Image = {
id: Scalars["String"]["output"];
kernel: Scalars["String"]["output"];
lastDeployed: Scalars["Time"]["output"];
latestTask?: Maybe<Task>;
name: Scalars["String"]["output"];
packages: Array<Package>;
toolchains: Array<Toolchain>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,50 @@ describe("GitHub app settings", () => {
saveButtonEnabled(false);
});

// TODO: Add test for deletion in DEVPROD-9282.
it("should be able to delete & save app credentials", () => {
cy.dataCy("github-app-id-input").as("appId");
cy.dataCy("github-private-key-input").as("privateKey");

it("should be able to save app credentials", () => {
cy.visit(getAppSettingsRoute("logkeeper"));
cy.contains("Token Permission Restrictions");
// Delete GitHub app credentials.
cy.dataCy("delete-app-credentials-button").should("be.visible");
cy.dataCy("delete-app-credentials-button").click();
cy.dataCy("delete-github-credentials-modal").should("be.visible");
cy.dataCy("delete-github-credentials-modal")
.find("button")
.contains("Delete")
.parent()
.click();
cy.validateToast("success");

cy.dataCy("github-app-credentials-banner").should("be.visible");
cy.dataCy("github-app-id-input").type("12345");
cy.dataCy("github-private-key-input").type("secret");
cy.get("@appId").should("have.value", "");
cy.get("@privateKey").should("have.value", "");
cy.get("@appId").should("have.attr", "aria-disabled", "false");
cy.get("@privateKey").should("have.attr", "aria-disabled", "false");

cy.reload();
cy.dataCy("github-app-credentials-banner").should("be.visible");
cy.get("@appId").should("have.value", "");
cy.get("@privateKey").should("have.value", "");

// Add GitHub app credentials.
cy.get("@appId").type("12345");
cy.get("@privateKey").type("secret");
cy.dataCy("save-settings-button").scrollIntoView();
saveButtonEnabled(true);
clickSave();
cy.validateToast("success", "Successfully updated project");

cy.dataCy("github-app-credentials-banner").should("not.exist");
cy.dataCy("github-app-id-input").should("have.value", "12345");
cy.dataCy("github-private-key-input").should("have.value", "{REDACTED}");
cy.dataCy("github-app-id-input").should(
"have.attr",
"aria-disabled",
"true",
);
cy.dataCy("github-private-key-input").should(
"have.attr",
"aria-disabled",
"true",
);
cy.get("@appId").should("have.value", "12345");
cy.get("@privateKey").should("have.value", "{REDACTED}");
cy.get("@appId").should("have.attr", "aria-disabled", "true");
cy.get("@privateKey").should("have.attr", "aria-disabled", "true");

cy.reload();
cy.dataCy("github-app-credentials-banner").should("not.exist");
cy.dataCy("github-app-id-input").should("have.value", "12345");
cy.dataCy("github-private-key-input").should("have.value", "{REDACTED}");
cy.get("@appId").should("have.value", "12345");
cy.get("@privateKey").should("have.value", "{REDACTED}");
});

it("should be able to save different permission groups for requesters, then return to defaults", () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/spruce/src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import styled from "@emotion/styled";
import Modal from "@leafygreen-ui/confirmation-modal";
import Modal, { Variant } from "@leafygreen-ui/confirmation-modal";
import { zIndex } from "constants/tokens";

export const ConfirmationModal = styled(Modal)`
const ConfirmationModal = styled(Modal)`
z-index: ${zIndex.modal};
`;
export { ConfirmationModal, Variant };
13 changes: 13 additions & 0 deletions apps/spruce/src/gql/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,7 @@ export type Image = {
id: Scalars["String"]["output"];
kernel: Scalars["String"]["output"];
lastDeployed: Scalars["Time"]["output"];
latestTask?: Maybe<Task>;
name: Scalars["String"]["output"];
packages: Array<Package>;
toolchains: Array<Toolchain>;
Expand Down Expand Up @@ -4878,6 +4879,18 @@ export type DeleteDistroMutation = {
deleteDistro: { __typename?: "DeleteDistroPayload"; deletedDistroId: string };
};

export type DeleteGithubAppCredentialsMutationVariables = Exact<{
projectId: Scalars["String"]["input"];
}>;

export type DeleteGithubAppCredentialsMutation = {
__typename?: "Mutation";
deleteGithubAppCredentials?: {
__typename?: "DeleteGithubAppCredentialsPayload";
oldAppId: number;
} | null;
};

export type DeleteProjectMutationVariables = Exact<{
projectId: Scalars["String"]["input"];
}>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation DeleteGithubAppCredentials($projectId: String!) {
deleteGithubAppCredentials(opts: { projectId: $projectId }) {
oldAppId
}
}
2 changes: 2 additions & 0 deletions apps/spruce/src/gql/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import CREATE_PUBLIC_KEY from "./create-public-key.graphql";
import DEACTIVATE_STEPBACK_TASK from "./deactivate-stepback-task.graphql";
import DEFAULT_SECTION_TO_REPO from "./default-section-to-repo.graphql";
import DELETE_DISTRO from "./delete-distro.graphql";
import DELETE_GITHUB_APP_CREDENTIALS from "./delete-github-app-credentials.graphql";
import DELETE_PROJECT from "./delete-project.graphql";
import DELETE_SUBSCRIPTIONS from "./delete-subscriptions.graphql";
import DETACH_PROJECT_FROM_REPO from "./detach-project-from-repo.graphql";
Expand Down Expand Up @@ -73,6 +74,7 @@ export {
DEACTIVATE_STEPBACK_TASK,
DEFAULT_SECTION_TO_REPO,
DELETE_DISTRO,
DELETE_GITHUB_APP_CREDENTIALS,
DELETE_PROJECT,
DELETE_SUBSCRIPTIONS,
DETACH_PROJECT_FROM_REPO,
Expand Down
2 changes: 2 additions & 0 deletions apps/spruce/src/pages/projectSettings/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ export const ProjectSettingsTabs: React.FC<Props> = ({
}
// @ts-expect-error: FIXME. This comment was added by an automated script.
identifier={identifier}
// @ts-expect-error: FIXME. This comment was added by an automated script.
projectId={projectId}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const AppSettingsTab: React.FC<TabProps> = ({
githubPermissionGroups,
identifier,
projectData,
projectId,
}) => {
const initialFormState = projectData;
const isAppDefined =
Expand All @@ -23,8 +24,9 @@ export const AppSettingsTab: React.FC<TabProps> = ({
githubPermissionGroups,
identifier,
isAppDefined,
projectId,
}),
[githubPermissionGroups, identifier, isAppDefined],
[githubPermissionGroups, identifier, isAppDefined, projectId],
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,11 @@
import { useRef } from "react";
import styled from "@emotion/styled";
import Banner from "@leafygreen-ui/banner";
import InlineDefinition from "@leafygreen-ui/inline-definition";
import { useLeafyGreenTable, LGColumnDef } from "@leafygreen-ui/table";
import { Body } from "@leafygreen-ui/typography";
import { ArrayFieldTemplateProps, Field } from "@rjsf/core";
import { StyledLink } from "components/styles";
import { ArrayFieldTemplateProps } from "@rjsf/core";
import { BaseTable } from "components/Table/BaseTable";
import { githubAppCredentialsDocumentationUrl } from "constants/externalResources";
import {
requesterToTitle,
requesterToDescription,
Requester,
} from "constants/requesters";
import { tableColumnOffset } from "constants/tokens";
import { Unpacked } from "types/utils";

export const RequesterTypeField: Field = ({
formData,
}: {
formData: Requester;
}) =>
requesterToDescription[formData] ? (
<InlineDefinition definition={requesterToDescription[formData]}>
{requesterToTitle[formData]}
</InlineDefinition>
) : (
<Body>{requesterToTitle[formData]}</Body>
);

export const GithubAppActions: Field = ({ uiSchema }) => {
const {
options: { isAppDefined },
} = uiSchema;

// TODO DEVPROD-9282: Add delete button.
return isAppDefined ? null : (
<Banner variant="warning" data-cy="github-app-credentials-banner">
App ID and Key must be saved in order for token permissions restrictions
to take effect. <br />
<StyledLink href={githubAppCredentialsDocumentationUrl}>
GitHub App Documentation
</StyledLink>
</Banner>
);
};

type ArrayItem = Unpacked<ArrayFieldTemplateProps["items"]>;

export const ArrayFieldTemplate: React.FC<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { MockedProvider } from "@apollo/client/testing";
import { FieldProps } from "@rjsf/core";
import { RenderFakeToastContext } from "context/toast/__mocks__";
import {
DeleteGithubAppCredentialsMutation,
DeleteGithubAppCredentialsMutationVariables,
} from "gql/generated/types";
import { DELETE_GITHUB_APP_CREDENTIALS } from "gql/mutations";
import { renderWithRouterMatch as render, screen, userEvent } from "test_utils";
import { ApolloMock } from "types/gql";
import { GithubAppActions } from ".";

const Field = ({ isAppDefined }: { isAppDefined: boolean }) => (
<MockedProvider mocks={[deleteAppCredentialsMock]}>
<GithubAppActions
{...({} as unknown as FieldProps)}
uiSchema={{
options: {
projectId: "evergreen",
isAppDefined,
},
}}
/>
</MockedProvider>
);

describe("githubAppActions", () => {
describe("app is not defined", () => {
it("renders the banner and not the button", () => {
const { Component } = RenderFakeToastContext(
<Field isAppDefined={false} />,
);
render(<Component />);
expect(
screen.getByDataCy("github-app-credentials-banner"),
).toBeInTheDocument();
expect(
screen.queryByDataCy("delete-app-credentials-button"),
).not.toBeInTheDocument();
});
});

describe("app is defined", () => {
it("renders the button and not the banner", () => {
const { Component } = RenderFakeToastContext(<Field isAppDefined />);
render(<Component />);
expect(
screen.getByDataCy("delete-app-credentials-button"),
).toBeInTheDocument();
expect(
screen.queryByDataCy("github-app-credentials-banner"),
).not.toBeInTheDocument();
});

it("can delete the credentials via the modal", async () => {
const user = userEvent.setup();

const { Component, dispatchToast } = RenderFakeToastContext(
<Field isAppDefined />,
);
render(<Component />);
await user.click(screen.getByDataCy("delete-app-credentials-button"));
expect(
screen.getByDataCy("delete-github-credentials-modal"),
).toBeInTheDocument();
const deleteButton = screen.getByRole("button", {
name: "Delete",
});
expect(deleteButton).toBeEnabled();
await user.click(deleteButton);
expect(dispatchToast.success).toHaveBeenCalledWith(
"GitHub app credentials were successfully deleted.",
);
});
});
});

const deleteAppCredentialsMock: ApolloMock<
DeleteGithubAppCredentialsMutation,
DeleteGithubAppCredentialsMutationVariables
> = {
request: {
query: DELETE_GITHUB_APP_CREDENTIALS,
variables: {
projectId: "evergreen",
},
},
result: {
data: {
deleteGithubAppCredentials: {
oldAppId: 12344,
},
},
},
};
Loading

0 comments on commit 0544693

Please sign in to comment.