Skip to content

Commit

Permalink
Add chains menu to certificate viewer dialog (#222)
Browse files Browse the repository at this point in the history
* Add chain tabs to certificate viewer dialog

* Add more tests

* Add & fix tests

* Change version

* Fix certificate props

---------

Co-authored-by: alex-slobodian <[email protected]>
  • Loading branch information
OleksandrSPV and aleksandr-slobodian authored Dec 5, 2024
1 parent 0b904ed commit b52b91f
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 61 deletions.
2 changes: 2 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export function App() {
dialog: certificateViewerDialog,
} = useCertificateViewerDialog({
providers,
currentProviderId,
fortifyClient,
});

const { open: handleProviderInfoDialogOpen, dialog: providerInfoDialog } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,76 @@ vi.mock("@peculiar/certificates-viewer-react", () => ({
}));

describe("<CertificateViewerDialog />", () => {
const certificate = {
raw: new ArrayBuffer(0),
subjectName: "Certificate name",
type: "x509",
label: "Certificate name",
subject: {
commonName: "Certificate name",
const certificates = [
{
raw: new ArrayBuffer(0),
subjectName: "Certificate name 1",
type: "x509",
label: "Certificate name 1",
subject: {
commonName: "Certificate name 1",
},
},
} as unknown as CertificateProps;
{
raw: new ArrayBuffer(0),
subjectName: "Certificate name 2",
type: "x509",
label: "Certificate name 2",
subject: {
commonName: "Certificate name 2",
},
},
] as unknown as CertificateProps[];

it("Should render as x509 and handle close", async () => {
const onCloseMock = vi.fn();

render(
<CertificateViewerDialog
certificate={certificate}
certificates={[certificates[0]]}
onClose={onCloseMock}
/>
);

expect(screen.getByText(`“${certificate.label}” details`));
expect(screen.getByText(/x509 certificate viewer component/));
expect(
screen.getByText(`“${certificates[0].label}” details`)
).toBeInTheDocument();
expect(
screen.getByText(/x509 certificate viewer component/)
).toBeInTheDocument();

await userEvent.click(screen.getByRole("button", { name: /Cancel/ }));
await userEvent.click(screen.getByRole("button", { name: /Close/ }));

expect(onCloseMock).toBeCalledTimes(1);
});

it("Should render & switches tabs", async () => {
render(
<CertificateViewerDialog certificates={certificates} onClose={vi.fn()} />
);

expect(
screen.getByText(`${certificates[0].label}`).closest("button")
).toHaveAttribute("aria-selected", "true");

await userEvent.click(screen.getByText(`${certificates[1].label}`));

expect(
screen.getByText(`${certificates[1].label}`).closest("button")
).toHaveAttribute("aria-selected", "true");
});

it("Should render as CSR", () => {
render(
<CertificateViewerDialog
certificate={
{ ...certificate, type: "csr" } as unknown as CertificateProps
}
certificates={[
{ ...certificates[0], type: "csr" } as unknown as CertificateProps,
]}
onClose={vi.fn()}
/>
);
expect(screen.getByText(/CSR certificate viewer component/));
expect(
screen.getByText(/CSR certificate viewer component/)
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,51 +1,91 @@
import React from "react";
import React, { useState } from "react";
import { Convert } from "pvtsutils";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Tab,
Tabs,
} from "@peculiar/react-components";
import {
PeculiarCertificateViewer,
PeculiarCsrViewer,
} from "@peculiar/certificates-viewer-react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { getCertificateName } from "../../utils/certificate";
import { CertificateProps, CertificateType } from "../../types";
import { CertificateProps } from "../../types";

import styles from "./styles/index.module.scss";

interface CertificateViewerDialogProps {
certificate: CertificateProps;
certificates: CertificateProps[];
onClose?: () => void;
}
export const CertificateViewerDialog: React.FunctionComponent<
CertificateViewerDialogProps
> = (props) => {
const { certificate, onClose } = props;
const { certificates, onClose } = props;
const { t } = useTranslation();
const [tabIndex, setTabIndex] = useState("0");

const certBase64 = Convert.ToBase64(certificate?.raw);
const renderTabs = () =>
certificates.length > 1 ? (
<div className={styles.dialog_title_tabs_wraper}>
<Tabs
value={tabIndex}
className={styles.dialog_title_tabs}
onChange={(_, value) => setTabIndex(value)}
>
{certificates.map((cert, index) => {
const name = getCertificateName(cert);
return (
<Tab key={`cert-tab-${name}-${index}`} id={index.toString()}>
{name}
</Tab>
);
})}
</Tabs>
</div>
) : null;

const renderContent = () =>
certificates[0]?.type === "x509" ? (
certificates.map((cert, index) => (
<PeculiarCertificateViewer
key={`cert-tab-cont-${index}`}
className={clsx(styles.dialog_tab_content, {
[styles.dialog_tab_content_current]: tabIndex === index.toString(),
})}
certificate={Convert.ToBase64(cert?.raw)}
download={true}
/>
))
) : (
<PeculiarCsrViewer
certificate={Convert.ToBase64(certificates[0]?.raw)}
download={true}
/>
);

return (
<Dialog open={true} onClose={onClose} className={styles.dialog}>
<DialogTitle>
{t("certificate-viewer-dialog.title", {
name: getCertificateName(certificate),
})}
<DialogTitle className={styles.dialog_title}>
<div className={styles.dialog_title_text}>
{t("certificate-viewer-dialog.title", {
name: getCertificateName(certificates[0]),
})}
</div>
{renderTabs()}
</DialogTitle>
<DialogContent className={styles.dialog_content}>
{(certificate?.type as CertificateType) === "csr" ? (
<PeculiarCsrViewer certificate={certBase64} download={true} />
) : (
<PeculiarCertificateViewer certificate={certBase64} download={true} />
)}
{renderContent()}
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
{t("button.cancel")}
{t("button.close")}
</Button>
</DialogActions>
</Dialog>
Expand Down
26 changes: 26 additions & 0 deletions src/components/certificate-viewer-dialog/styles/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,29 @@
padding: 0;
}
}

.dialog_title_text {
padding: var(--pv-size-base-3) var(--pv-size-base-4);
}

.dialog_title_tabs_wraper {
border-top: 1px solid var(--pv-color-gray-4);
max-width: 870px;
overflow: auto;
.dialog_title_tabs {
padding: 0 var(--pv-size-base-4);
}
}

.dialog_title {
padding: 0 !important;
display: flex;
flex-direction: column;
}

.dialog_tab_content {
display: none;
}
.dialog_tab_content_current {
display: block;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { renderHook, act } from "@testing";
import { vi } from "vitest";
import { useCertificateViewerDialog } from "./useCertificateViewerDialog";
import { CertificateProps } from "../../types";

import type { IProviderInfo } from "@peculiar/fortify-client-core";
import type { IProviderInfo, FortifyAPI } from "@peculiar/fortify-client-core";

vi.mock("@peculiar/x509", async (importOriginal) => {
const originalModule =
await importOriginal<typeof import("@peculiar/x509")>();

return {
...originalModule,
X509Certificate: vi.fn().mockImplementation(() => ({
rawData: new ArrayBuffer(0),
subject: "Test Subject",
subjectName: "Rest Subject Name",
})),
};
});

describe("useCertificateViewerDialog", () => {
const providers = [
Expand All @@ -15,18 +30,23 @@ describe("useCertificateViewerDialog", () => {
const certificate = {
id: "1",
label: "Certificate name",
type: "x509",
} as CertificateProps;

const defaultProps = {
providers,
fortifyClient: null,
currentProviderId: providers[0].id,
};

const defaultOpenProps = {
certificate,
providerId: providers[0].id,
};

it("Should initialize, open & close dialog", () => {
const { result } = renderHook(() =>
useCertificateViewerDialog({
providers,
})
useCertificateViewerDialog(defaultProps)
);

expect(result.current.dialog).toBeInstanceOf(Function);
Expand All @@ -39,34 +59,56 @@ describe("useCertificateViewerDialog", () => {
const DialogComponent = result.current.dialog();

expect(DialogComponent).not.toBeNull();
expect(DialogComponent?.props.certificate).toBe(certificate);

act(() => {
DialogComponent?.props.onClose();
});
expect(DialogComponent?.props.certificates).toStrictEqual([certificate]);

DialogComponent?.props.onClose();
expect(result.current.dialog()).toBeNull();
});

it("Should close dialog if current provider is not found", async () => {
const { result, rerender } = renderHook(
(localProviders) =>
useCertificateViewerDialog({
providers: localProviders,
}),
{ initialProps: providers }
it("Should close the dialog when the current provider is not found", () => {
const { result } = renderHook(() =>
useCertificateViewerDialog(defaultProps)
);

act(() => {
result.current.open(defaultOpenProps);
result.current.open({
...defaultOpenProps,
providerId: "2",
});
});

rerender([
{
id: "2",
},
] as IProviderInfo[]);
const DialogComponent = result.current.dialog();
expect(DialogComponent).toBeNull();
});

expect(result.current.dialog()).toBeNull();
it("Should make a chain of certificates", async () => {
const mockFortifyClient: Partial<FortifyAPI> = {
getProviderById: vi.fn().mockResolvedValue({
certStorage: {
getChain: vi
.fn()
.mockResolvedValue([
{ value: new ArrayBuffer(0) },
{ value: new ArrayBuffer(0) },
]),
},
}),
};

const { result } = renderHook(() =>
useCertificateViewerDialog({
...defaultProps,
fortifyClient: mockFortifyClient as FortifyAPI,
})
);

await act(async () => {
await result.current.open(defaultOpenProps);
});

const DialogComponent = result.current.dialog();
expect(mockFortifyClient.getProviderById).toHaveBeenCalled();
expect(DialogComponent).not.toBeNull();
expect(DialogComponent?.props.certificates.length).toBe(2);
});
});
Loading

0 comments on commit b52b91f

Please sign in to comment.