diff --git a/packages/app/src/background/services/approval/__tests__/approvalService.test.ts b/packages/app/src/background/services/approval/__tests__/approvalService.test.ts index 10cc623d3..67cd11d59 100644 --- a/packages/app/src/background/services/approval/__tests__/approvalService.test.ts +++ b/packages/app/src/background/services/approval/__tests__/approvalService.test.ts @@ -215,6 +215,14 @@ describe("background/services/approval", () => { "CryptKeeper: origin is not approved", ); }); + + test("should throw error if origin is not provided", async () => { + await approvalService.unlock(); + + expect(() => approvalService.isOriginApproved({}, { urlOrigin: "" })).toThrowError( + "CryptKeeper: origin is not set", + ); + }); }); describe("backup", () => { diff --git a/packages/app/src/config/mock/file.ts b/packages/app/src/config/mock/file.ts index be9be9a65..86f708c20 100644 --- a/packages/app/src/config/mock/file.ts +++ b/packages/app/src/config/mock/file.ts @@ -1,5 +1,21 @@ export const mockJsonFile = new File([JSON.stringify({ ping: true })], "ping.json", { type: "application/json" }); +export const mockIdenityJsonFile = new File([JSON.stringify({ trapdoor: "1", nullifier: "1" })], "identity.json", { + type: "application/json", +}); + +export const mockArrayIdenityJsonFile = new File([JSON.stringify(["2", "2"])], "identity.json", { + type: "application/json", +}); + +export const mockIdenityPrivateJsonFile = new File( + [JSON.stringify({ _trapdoor: "3", _nullifier: "3" })], + "identity.json", + { type: "application/json" }, +); + +export const mockEmptyJsonFile = new File([""], "empty.json", { type: "application/json" }); + interface IDataTransfer { dataTransfer: { files: File[]; diff --git a/packages/app/src/ui/components/UploadButton/UploadButton.tsx b/packages/app/src/ui/components/UploadButton/UploadButton.tsx new file mode 100644 index 000000000..f095b6b26 --- /dev/null +++ b/packages/app/src/ui/components/UploadButton/UploadButton.tsx @@ -0,0 +1,47 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import { forwardRef, Ref, type HTMLAttributes } from "react"; + +import type { Accept } from "react-dropzone"; + +import { type onDropCallback, useUploadButton } from "./useUploadButton"; + +export interface IUploadButtonProps extends Omit, "onDrop"> { + isLoading?: boolean; + errorMessage?: string; + multiple?: boolean; + accept: Accept; + name: string; + onDrop: onDropCallback; +} + +const UploadButtonUI = ( + { isLoading = false, multiple = true, errorMessage = "", accept, name, onDrop, ...rest }: IUploadButtonProps, + ref: Ref, +): JSX.Element => { + const { isDragActive, getRootProps, getInputProps } = useUploadButton({ + isLoading, + accept, + multiple, + onDrop, + }); + + const fileTitle = multiple ? "files" : "file"; + + return ( + + + + + + + {errorMessage} + + + ); +}; + +export const UploadButton = forwardRef(UploadButtonUI); diff --git a/packages/app/src/ui/components/UploadButton/__tests__/UploadButton.test.tsx b/packages/app/src/ui/components/UploadButton/__tests__/UploadButton.test.tsx new file mode 100644 index 000000000..6ba65264c --- /dev/null +++ b/packages/app/src/ui/components/UploadButton/__tests__/UploadButton.test.tsx @@ -0,0 +1,62 @@ +/** + * @jest-environment jsdom + */ + +import { render } from "@testing-library/react"; + +import { IUploadButtonProps, UploadButton } from ".."; +import { IUseUploadButtonData, useUploadButton } from "../useUploadButton"; + +jest.mock("../useUploadButton", (): unknown => ({ + ...jest.requireActual("../useUploadButton"), + useUploadButton: jest.fn(), +})); + +describe("ui/components/UploadButton", () => { + const defaultHookData: IUseUploadButtonData = { + isDragActive: false, + acceptedFiles: [], + getInputProps: jest.fn(), + getRootProps: jest.fn(), + }; + + const defaultProps: IUploadButtonProps = { + accept: { "application/json": [".json"] }, + name: "file", + onDrop: jest.fn(), + }; + + beforeEach(() => { + (useUploadButton as jest.Mock).mockReturnValue(defaultHookData); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should render properly", async () => { + const { findByText } = render(); + + const dragText = await findByText("Upload files"); + + expect(dragText).toBeInTheDocument(); + }); + + test("should render properly while drag is active", async () => { + (useUploadButton as jest.Mock).mockReturnValue({ ...defaultHookData, isDragActive: true }); + + const { findByText } = render(); + + const dragText = await findByText("Drop the files here..."); + + expect(dragText).toBeInTheDocument(); + }); + + test("should render error properly", async () => { + const { findByText } = render(); + + const error = await findByText("error"); + + expect(error).toBeInTheDocument(); + }); +}); diff --git a/packages/app/src/ui/components/UploadButton/__tests__/useUploadButton.test.tsx b/packages/app/src/ui/components/UploadButton/__tests__/useUploadButton.test.tsx new file mode 100644 index 000000000..b13d156b8 --- /dev/null +++ b/packages/app/src/ui/components/UploadButton/__tests__/useUploadButton.test.tsx @@ -0,0 +1,37 @@ +/** + * @jest-environment jsdom + */ + +import { act, fireEvent, render, renderHook } from "@testing-library/react"; + +import { createDataTransfer, mockJsonFile } from "@src/config/mock/file"; + +import { IUseUploadButtonArgs, useUploadButton } from "../useUploadButton"; + +describe("ui/components/UploadButton/useUploadButton", () => { + const defaultHookArgs: IUseUploadButtonArgs = { + isLoading: false, + accept: { "application/json": [".json"] }, + onDrop: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should upload file properly", async () => { + const data = createDataTransfer([mockJsonFile]); + + const { result } = renderHook(() => useUploadButton(defaultHookArgs)); + + const { container } = render( +
+ +
, + ); + + await act(() => fireEvent.drop(container.querySelector("div")!, data)); + + expect(defaultHookArgs.onDrop).toBeCalledTimes(1); + }); +}); diff --git a/packages/app/src/ui/components/UploadButton/index.ts b/packages/app/src/ui/components/UploadButton/index.ts new file mode 100644 index 000000000..b57faf798 --- /dev/null +++ b/packages/app/src/ui/components/UploadButton/index.ts @@ -0,0 +1,2 @@ +export { type IUploadButtonProps, UploadButton } from "./UploadButton"; +export { type onDropCallback, type IUseUploadButtonArgs, type IUseUploadButtonData } from "./useUploadButton"; diff --git a/packages/app/src/ui/components/UploadButton/useUploadButton.ts b/packages/app/src/ui/components/UploadButton/useUploadButton.ts new file mode 100644 index 000000000..02496a58f --- /dev/null +++ b/packages/app/src/ui/components/UploadButton/useUploadButton.ts @@ -0,0 +1,45 @@ +import { type HTMLAttributes } from "react"; +import { + type DropzoneInputProps, + type DropzoneRootProps, + type FileRejection, + type Accept, + useDropzone, +} from "react-dropzone"; + +export interface IUseUploadButtonArgs { + isLoading: boolean; + accept: Accept; + multiple?: boolean; + onDrop: onDropCallback; +} + +export interface IUseUploadButtonData { + isDragActive: boolean; + acceptedFiles: File[]; + getInputProps: (props?: DropzoneInputProps) => HTMLAttributes; + getRootProps: (props?: DropzoneRootProps) => HTMLAttributes; +} + +export type onDropCallback = (acceptedFiles: File[], fileRejections: FileRejection[]) => Promise; + +export const useUploadButton = ({ + isLoading, + accept, + multiple = true, + onDrop, +}: IUseUploadButtonArgs): IUseUploadButtonData => { + const { isDragActive, acceptedFiles, getRootProps, getInputProps } = useDropzone({ + accept, + disabled: isLoading, + multiple, + onDrop, + }); + + return { + isDragActive, + acceptedFiles, + getRootProps, + getInputProps, + }; +}; diff --git a/packages/app/src/ui/components/UploadInput/UploadInput.tsx b/packages/app/src/ui/components/UploadInput/UploadInput.tsx index 225702270..c12650240 100644 --- a/packages/app/src/ui/components/UploadInput/UploadInput.tsx +++ b/packages/app/src/ui/components/UploadInput/UploadInput.tsx @@ -16,7 +16,7 @@ export interface IUploadInputProps extends Omit onDrop: onDropCallback; } -export const UploadInputUI = ( +const UploadInputUI = ( { isLoading = false, multiple = true, errorMessage = "", accept, onDrop, ...rest }: IUploadInputProps, ref: Ref, ): JSX.Element => { diff --git a/packages/app/src/ui/components/UploadInput/__tests__/UploadInput.test.tsx b/packages/app/src/ui/components/UploadInput/__tests__/UploadInput.test.tsx index cb1c87f11..4c632c976 100644 --- a/packages/app/src/ui/components/UploadInput/__tests__/UploadInput.test.tsx +++ b/packages/app/src/ui/components/UploadInput/__tests__/UploadInput.test.tsx @@ -23,7 +23,7 @@ describe("ui/components/UploadInput", () => { }; const defaultProps: IUploadInputProps = { - accept: { "application/json": [] }, + accept: { "application/json": [".json"] }, onDrop: jest.fn(), }; diff --git a/packages/app/src/ui/components/UploadInput/__tests__/useUploadInput.test.tsx b/packages/app/src/ui/components/UploadInput/__tests__/useUploadInput.test.tsx index 204bd3531..78c4a8f6f 100644 --- a/packages/app/src/ui/components/UploadInput/__tests__/useUploadInput.test.tsx +++ b/packages/app/src/ui/components/UploadInput/__tests__/useUploadInput.test.tsx @@ -11,7 +11,7 @@ import { IUseUploadInputArgs, useUploadInput } from "../useUploadInput"; describe("ui/components/UploadInput/useUploadInput", () => { const defaultHookArgs: IUseUploadInputArgs = { isLoading: false, - accept: { "application/json": [] }, + accept: { "application/json": [".json"] }, onDrop: jest.fn(), }; diff --git a/packages/app/src/ui/pages/ImportIdentity/ImportIdentity.tsx b/packages/app/src/ui/pages/ImportIdentity/ImportIdentity.tsx index ceb5bf509..cefc60ce2 100644 --- a/packages/app/src/ui/pages/ImportIdentity/ImportIdentity.tsx +++ b/packages/app/src/ui/pages/ImportIdentity/ImportIdentity.tsx @@ -5,23 +5,13 @@ import Typography from "@mui/material/Typography"; import { BigNumberInput } from "@src/ui/components/BigNumberInput"; import { FullModalContent, FullModalFooter, FullModalHeader } from "@src/ui/components/FullModal"; import { Input } from "@src/ui/components/Input"; +import { UploadButton } from "@src/ui/components/UploadButton"; import { useImportIdentity } from "./useImportIdentity"; const ImportIdentity = (): JSX.Element => { - const { - isLoading, - urlOrigin, - errors, - secret, - commitment, - trapdoor, - nullifier, - register, - onGoBack, - onGoToHost, - onSubmit, - } = useImportIdentity(); + const { isLoading, urlOrigin, errors, trapdoor, nullifier, register, onGoBack, onGoToHost, onDrop, onSubmit } = + useImportIdentity(); return ( { {urlOrigin && ( - + { )} {!urlOrigin && ( - + Import identity with trapdoor and nullifier )} - + { /> + + + Or enter data manually + { })} /> - - - - - - - - diff --git a/packages/app/src/ui/pages/ImportIdentity/__tests__/ImportIdentity.test.tsx b/packages/app/src/ui/pages/ImportIdentity/__tests__/ImportIdentity.test.tsx index 5bba2b928..d079cee5b 100644 --- a/packages/app/src/ui/pages/ImportIdentity/__tests__/ImportIdentity.test.tsx +++ b/packages/app/src/ui/pages/ImportIdentity/__tests__/ImportIdentity.test.tsx @@ -30,6 +30,7 @@ describe("ui/pages/ImportIdentity", () => { register: jest.fn(), onGoBack: jest.fn(), onGoToHost: jest.fn(), + onDrop: jest.fn(), onSubmit: jest.fn(), }; diff --git a/packages/app/src/ui/pages/ImportIdentity/__tests__/useImportIdentity.test.ts b/packages/app/src/ui/pages/ImportIdentity/__tests__/useImportIdentity.test.ts index 5ba582819..68787c4da 100644 --- a/packages/app/src/ui/pages/ImportIdentity/__tests__/useImportIdentity.test.ts +++ b/packages/app/src/ui/pages/ImportIdentity/__tests__/useImportIdentity.test.ts @@ -7,6 +7,13 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import { getLinkPreview } from "link-preview-js"; import { useNavigate } from "react-router-dom"; +import { + mockArrayIdenityJsonFile, + mockEmptyJsonFile, + mockIdenityJsonFile, + mockIdenityPrivateJsonFile, + mockJsonFile, +} from "@src/config/mock/file"; import { mockDefaultIdentity, mockDefaultIdentityCommitment, @@ -148,6 +155,74 @@ describe("ui/pages/ImportIdentity/useImportIdentity", () => { expect(redirectToNewTab).toBeCalledWith(mockDefaultIdentity.metadata.urlOrigin); }); + test("should drop object file properly", async () => { + const acceptedFiles = [mockIdenityJsonFile]; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop(acceptedFiles, [])); + + expect(result.current.errors.root).toBeUndefined(); + expect(result.current.trapdoor).toBe("1"); + expect(result.current.nullifier).toBe("1"); + }); + + test("should drop array file properly", async () => { + const acceptedFiles = [mockArrayIdenityJsonFile]; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop(acceptedFiles, [])); + + expect(result.current.errors.root).toBeUndefined(); + expect(result.current.trapdoor).toBe("2"); + expect(result.current.nullifier).toBe("2"); + }); + + test("should drop private object file properly", async () => { + const acceptedFiles = [mockIdenityPrivateJsonFile]; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop(acceptedFiles, [])); + + expect(result.current.errors.root).toBeUndefined(); + expect(result.current.trapdoor).toBe("3"); + expect(result.current.nullifier).toBe("3"); + }); + + test("should handle empty file properly", async () => { + const acceptedFiles = [mockEmptyJsonFile]; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop(acceptedFiles, [])); + + expect(result.current.errors.root).toBe("File is empty"); + }); + + test("should drop files and handle reject errors properly", async () => { + const rejectedFiles = [{ file: mockJsonFile, errors: [{ code: "code", message: "error" }] }]; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop([], rejectedFiles)); + + expect(result.current.errors.root).toBe("error"); + }); + + test("should handle file read error properly", async () => { + const acceptedFiles: File[] = []; + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.onDrop(acceptedFiles, [])); + + expect(result.current.errors.root).toBe( + "Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'.", + ); + }); + test("should submit properly", async () => { const { result } = renderHook(() => useImportIdentity()); @@ -179,6 +254,22 @@ describe("ui/pages/ImportIdentity/useImportIdentity", () => { ); }); + test("should submit and go home properly", async () => { + (useSearchParam as jest.Mock).mockImplementation((arg: string) => + arg === "urlOrigin" ? mockDefaultIdentity.metadata.urlOrigin : Paths.CREATE_IDENTITY, + ); + + const { result } = renderHook(() => useImportIdentity()); + + await act(() => result.current.register("name").onChange({ target: { value: "name" } })); + await act(() => Promise.resolve(result.current.onSubmit())); + + expect(mockDispatch).toBeCalledTimes(1); + expect(importIdentity).toBeCalledTimes(1); + expect(mockNavigate).toBeCalledTimes(1); + expect(mockNavigate).toBeCalledWith(Paths.HOME); + }); + test("should handle submit error properly", async () => { const error = new Error("error"); (useAppDispatch as jest.Mock).mockReturnValue(jest.fn(() => Promise.reject(error))); diff --git a/packages/app/src/ui/pages/ImportIdentity/useImportIdentity.ts b/packages/app/src/ui/pages/ImportIdentity/useImportIdentity.ts index 0744a2a5d..6cb5a764f 100644 --- a/packages/app/src/ui/pages/ImportIdentity/useImportIdentity.ts +++ b/packages/app/src/ui/pages/ImportIdentity/useImportIdentity.ts @@ -1,5 +1,6 @@ import { EventName } from "@cryptkeeperzk/providers"; import { calculateIdentityCommitment, calculateIdentitySecret } from "@cryptkeeperzk/zk"; +import get from "lodash/get"; import { useCallback, useMemo } from "react"; import { UseFormRegister, useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; @@ -13,8 +14,12 @@ import { rejectUserRequest } from "@src/ui/ducks/requests"; import { useSearchParam } from "@src/ui/hooks/url"; import { useValidationResolver } from "@src/ui/hooks/validation"; import { redirectToNewTab } from "@src/util/browser"; +import { readFile } from "@src/util/file"; import { checkBigNumber, convertFromHexToDec } from "@src/util/numbers"; +import type { onDropCallback } from "@src/ui/components/UploadButton"; +import type { FileRejection } from "react-dropzone"; + export interface IUseImportIdentityData { isLoading: boolean; errors: Partial<{ @@ -31,6 +36,7 @@ export interface IUseImportIdentityData { register: UseFormRegister; onGoBack: () => void; onGoToHost: () => void; + onDrop: onDropCallback; onSubmit: () => void; } @@ -72,8 +78,10 @@ export const useImportIdentity = (): IUseImportIdentityData => { const { formState: { isSubmitting, isLoading, errors }, setError, + setValue, register, watch, + clearErrors, handleSubmit, } = useForm({ defaultValues: { @@ -108,11 +116,56 @@ export const useImportIdentity = (): IUseImportIdentityData => { redirectToNewTab(urlOrigin!); }, [urlOrigin]); + const onDrop = useCallback( + async (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + if (rejectedFiles[0]) { + setError("root", { message: rejectedFiles[0].errors[0].message }); + return; + } + + clearErrors(); + await readFile(acceptedFiles[0]) + .then((res) => { + const text = res.target?.result; + + if (!text) { + setError("root", { message: "File is empty" }); + } + + return text?.toString(); + }) + .then((content?: string) => { + if (!content) { + return; + } + + const parsed = JSON.parse(content) as + | Partial<{ trapdoor: string; nullifier: string; _trapdoor: string; _nullifier: string }> + | [string, string]; + + if (Array.isArray(parsed)) { + setValue("trapdoor", parsed[0]); + setValue("nullifier", parsed[1]); + return; + } + + setValue("trapdoor", get(parsed, "trapdoor", "") || get(parsed, "_trapdoor", "")); + setValue("nullifier", get(parsed, "nullifier", "") || get(parsed, "_nullifier", "")); + }) + .catch((error: Error) => { + setError("root", { message: error.message }); + }); + }, + [setValue, setError, clearErrors], + ); + const onSubmit = useCallback( (data: FormFields) => { dispatch(importIdentity({ ...data, urlOrigin })) .then(() => { - if (redirectUrl) { + if (redirect === Paths.CREATE_IDENTITY.toString()) { + navigate(Paths.HOME); + } else if (redirectUrl) { navigate(redirectUrl); } else { dispatch(closePopup()).then(() => { @@ -124,7 +177,7 @@ export const useImportIdentity = (): IUseImportIdentityData => { setError("root", { message: error.message }); }); }, - [redirectUrl, urlOrigin, setError, dispatch], + [redirect, redirectUrl, urlOrigin, setError, dispatch], ); return { @@ -143,6 +196,7 @@ export const useImportIdentity = (): IUseImportIdentityData => { register, onGoBack, onGoToHost, + onDrop, onSubmit: handleSubmit(onSubmit), }; }; diff --git a/packages/app/src/ui/pages/OnboardingBackup/OnboardingBackup.tsx b/packages/app/src/ui/pages/OnboardingBackup/OnboardingBackup.tsx index 0ebd6fc9e..cc41973e2 100644 --- a/packages/app/src/ui/pages/OnboardingBackup/OnboardingBackup.tsx +++ b/packages/app/src/ui/pages/OnboardingBackup/OnboardingBackup.tsx @@ -45,7 +45,7 @@ const OnboardingBackup = (): JSX.Element => { { { const cryptKeeper = await connectCryptKeeper(page); await cryptKeeper.createAccount({ password, mnemonic }); await cryptKeeper.approve(); - await cryptKeeper.connectIdentity(); + await cryptKeeper.connectIdentity(0, isImport); await cryptKeeper.close(); } diff --git a/packages/e2e/pages/cryptKeeper/ConnectIdentity.ts b/packages/e2e/pages/cryptKeeper/ConnectIdentity.ts index cd05b3e81..e4513ce3d 100644 --- a/packages/e2e/pages/cryptKeeper/ConnectIdentity.ts +++ b/packages/e2e/pages/cryptKeeper/ConnectIdentity.ts @@ -14,7 +14,13 @@ export default class ConnectIdentity extends BasePage { async createIdentity(params: ICreateIdentityArgs): Promise { await this.page.getByTestId("create-new-identity").click(); - await this.identities.createIdentity(this.page, params); + + if (!params.isImport) { + await this.identities.createIdentity(params, this.page); + } else { + await this.page.getByTestId("import-identity").click(); + await this.identities.importIdentity({ name: "Account Zero", trapdoor: "0", nullifier: "0" }, this.page); + } return this; } diff --git a/packages/e2e/pages/cryptKeeper/CryptKeeper.ts b/packages/e2e/pages/cryptKeeper/CryptKeeper.ts index 567d26b09..a5223c50c 100644 --- a/packages/e2e/pages/cryptKeeper/CryptKeeper.ts +++ b/packages/e2e/pages/cryptKeeper/CryptKeeper.ts @@ -94,11 +94,11 @@ export default class CryptKeeper extends BasePage { await this.page.getByText("Approve").click(); } - async connectIdentity(index = 0): Promise { + async connectIdentity(index = 0, isImport = false): Promise { const cryptKeeper = await this.page.context().waitForEvent("page"); await new ConnectIdentity(cryptKeeper, this.identities) - .createIdentity({ walletType: "ck", nonce: 0, isDeterministic: false }) + .createIdentity({ walletType: "ck", nonce: 0, isDeterministic: false, isImport }) .then((page) => page.selectIdentity(index)); } diff --git a/packages/e2e/pages/cryptKeeper/Identities.ts b/packages/e2e/pages/cryptKeeper/Identities.ts index 24c5d906f..82ef333c6 100644 --- a/packages/e2e/pages/cryptKeeper/Identities.ts +++ b/packages/e2e/pages/cryptKeeper/Identities.ts @@ -6,6 +6,7 @@ export interface ICreateIdentityArgs { walletType: WalletType; nonce: number; isDeterministic: boolean; + isImport?: boolean; } export interface IUpdateIdentityArgs { @@ -18,6 +19,11 @@ export interface IImportIdentityArgs { nullifier?: string; } +export interface IImportIdentityWithFileArgs { + name: string; + filepath: string; +} + type WalletType = "eth" | "ck"; export default class Identities extends BasePage { @@ -46,10 +52,13 @@ export default class Identities extends BasePage { async createIdentityFromHome(params: ICreateIdentityArgs): Promise { await this.page.getByTestId("create-new-identity").click({ delay: 1000 }); - await this.createIdentity(this.page, params); + await this.createIdentity(params); } - async createIdentity(page: Page, { nonce, walletType, isDeterministic }: ICreateIdentityArgs): Promise { + async createIdentity( + { nonce, walletType, isDeterministic }: ICreateIdentityArgs, + page: Page | undefined = this.page, + ): Promise { await page.getByLabel("Nonce", { exact: true }).fill(nonce.toString()); const deterministicCheckbox = page.getByLabel("Deterministic identity", { exact: true }); @@ -97,17 +106,27 @@ export default class Identities extends BasePage { await this.page.getByTestId("import-identity").click(); } - async importIdentity({ name, trapdoor, nullifier }: IImportIdentityArgs): Promise { + async importIdentityWithFile({ name, filepath }: IImportIdentityWithFileArgs): Promise { await this.page.getByLabel("Name").fill(name); + await this.page.setInputFiles(`input[name="file"]`, filepath); + + await this.page.getByTestId("import-identity").click(); + } + + async importIdentity( + { name, trapdoor, nullifier }: IImportIdentityArgs, + page: Page | undefined = this.page, + ): Promise { + await page.getByLabel("Name").fill(name); if (trapdoor) { - await this.page.getByLabel("Trapdoor").fill(trapdoor); + await page.getByLabel("Trapdoor").fill(trapdoor); } if (nullifier) { - await this.page.getByLabel("Nullifier").fill(nullifier); + await page.getByLabel("Nullifier").fill(nullifier); } - await this.page.getByTestId("import-identity").click(); + await page.getByTestId("import-identity").click(); } } diff --git a/packages/e2e/tests/importExternalIdentity.test.ts b/packages/e2e/tests/importExternalIdentity.test.ts index f7751c5a4..ab37a2b8e 100644 --- a/packages/e2e/tests/importExternalIdentity.test.ts +++ b/packages/e2e/tests/importExternalIdentity.test.ts @@ -1,3 +1,5 @@ +import path from "path"; + import type { IImportIdentityArgs } from "../pages/cryptKeeper/Identities"; import type { Page } from "@playwright/test"; @@ -14,7 +16,7 @@ test.describe("import external identity", () => { }; test.beforeEach(async ({ page, cryptKeeperExtensionId, context }) => { - await createAccount({ page, cryptKeeperExtensionId, context }); + await createAccount({ page, cryptKeeperExtensionId, context, isImport: true }); await page.goto(`chrome-extension://${cryptKeeperExtensionId}/popup.html`); await expect(page.getByTestId("home-page")).toBeVisible(); @@ -32,10 +34,9 @@ test.describe("import external identity", () => { const cryptKeeper = new CryptKeeper(page); await cryptKeeper.identities.goToImportIdentity(); - await cryptKeeper.identities.importIdentity({ name: "Test #0", trapdoor: "0", nullifier: "0" }); - await cryptKeeper.getByText("Reject", { exact: true }).click(); + await cryptKeeper.identities.importIdentity({ name: "Test #1", trapdoor: "1", nullifier: "1" }); - await expect(cryptKeeper.getByText(/Test #0/)).toHaveCount(1); + await expect(cryptKeeper.getByText(/Test #1/)).toHaveCount(1); }); test("should import identity from demo properly [health-check]", async ({ page, cryptKeeperExtensionId }) => { @@ -65,4 +66,47 @@ test.describe("import external identity", () => { await expect(page.getByText(/Test #3/)).toHaveCount(1); await expect(page.getByText(/Test #4/)).toHaveCount(1); }); + + test("should import identity with json files properly [health-check]", async ({ page, cryptKeeperExtensionId }) => { + await page.goto(`chrome-extension://${cryptKeeperExtensionId}/popup.html`); + await expect(page.getByTestId("home-page")).toBeVisible(); + + const cryptKeeper = new CryptKeeper(page); + + const invalidBackups = [ + path.resolve(__dirname, "../backups/12_invalid_identity.json"), + path.resolve(__dirname, "../backups/13_empty_identity.json"), + ]; + + await cryptKeeper.identities.goToImportIdentity(); + + /* eslint-disable no-await-in-loop,no-restricted-syntax */ + for (const backupFilePath of invalidBackups) { + await cryptKeeper.identities.importIdentityWithFile({ + name: "Test", + filepath: backupFilePath, + }); + } + /* eslint-enable no-await-in-loop */ + await cryptKeeper.getByText("Reject", { exact: true }).click(); + await cryptKeeper.getByText("Reject", { exact: true }).click(); + + const backupFilePaths = [ + path.resolve(__dirname, "../backups/9_identity.json"), + path.resolve(__dirname, "../backups/10_identity.json"), + path.resolve(__dirname, "../backups/11_identity.json"), + ]; + + /* eslint-disable no-await-in-loop,no-restricted-syntax */ + for (const backupFilePath of backupFilePaths) { + await cryptKeeper.identities.goToImportIdentity(); + await cryptKeeper.identities.importIdentityWithFile({ + name: "Test", + filepath: backupFilePath, + }); + } + /* eslint-enable no-await-in-loop */ + + await expect(cryptKeeper.getByText(/Test/)).toHaveCount(3); + }); });