Skip to content

Commit

Permalink
Add url validation (#2016)
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan authored Oct 9, 2024
1 parent a985a89 commit 8a9deb1
Show file tree
Hide file tree
Showing 32 changed files with 490 additions and 96 deletions.
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"vite": "^5.4.8",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-node-polyfills": "^0.17.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"@hookform/resolvers": "^3.9.0"
},
"packageManager": "pnpm@9.9.0",
"dependencies": {
Expand Down
66 changes: 50 additions & 16 deletions apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ beforeEach(() => {
});

describe("<UpsertNetworkModal />", () => {
const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc.com",
tzktApiUrl: "https://tzkt.com",
tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};

describe("edit mode", () => {
beforeEach(() => {
store.dispatch(networksActions.upsertNetwork(customNetwork));
Expand All @@ -27,14 +35,6 @@ describe("<UpsertNetworkModal />", () => {
const user = userEvent.setup();
render(<UpsertNetworkModal network={customNetwork} />, { store });

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -55,14 +55,6 @@ describe("<UpsertNetworkModal />", () => {
const user = userEvent.setup();
render(<UpsertNetworkModal network={customNetwork} />, { store });

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -85,6 +77,48 @@ describe("<UpsertNetworkModal />", () => {
});
});

describe("URL fields validation", () => {
const urlFields = [
{ label: "RPC URL", required: true },
{ label: "Tzkt API URL", required: true },
{ label: "Tzkt Explorer URL", required: true },
{ label: "Buy Tez URL", required: false },
];

it.each(urlFields)("validates $label field", async ({ label, required }) => {
const user = userEvent.setup();
render(<UpsertNetworkModal />, { store });

await user.type(screen.getByLabelText(label), "invalid-url");
await user.tab();

await waitFor(() => {
expect(screen.getByText(`Enter a valid ${label}`)).toBeVisible();
});

await user.clear(screen.getByLabelText(label));
await user.tab();

if (required) {
await waitFor(() => {
expect(screen.getByText(`${label} is required`)).toBeVisible();
});
} else {
await waitFor(() => {
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
}

await user.type(screen.getByLabelText(label), "https://valid-url.com");
await user.tab();

await waitFor(() => {
expect(screen.queryByText(`Enter a valid ${label}`)).not.toBeInTheDocument();
});
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
});

describe("create mode", () => {
describe("name field", () => {
it("validates uniqueness", async () => {
Expand Down
22 changes: 10 additions & 12 deletions apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
ModalFooter,
ModalHeader,
} from "@chakra-ui/react";
import { useDynamicModalContext } from "@umami/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { getNetworkValidationScheme, useDynamicModalContext } from "@umami/components";
import { networksActions, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
Expand All @@ -30,7 +31,11 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
formState: { errors, isValid },
register,
handleSubmit,
} = useForm<Network>({ mode: "onBlur", defaultValues: network });
} = useForm<Network>({
mode: "onBlur",
defaultValues: network,
resolver: zodResolver(getNetworkValidationScheme(availableNetworks, network)),
});

const onSubmit = (network: Network) => {
dispatch(networksActions.upsertNetwork(network));
Expand All @@ -51,12 +56,7 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="mainnet"
{...register("name", {
required: "Name is required",
validate: name => {
if (availableNetworks.find(n => n.name === name)) {
return "Network with this name already exists";
}
},
setValueAs: removeTrailingSlashes,
})}
/>
{errors.name && <FormErrorMessage>{errors.name.message}</FormErrorMessage>}
Expand All @@ -67,7 +67,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://prod.tcinfra.net/rpc/mainnet"
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -78,7 +77,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://api.ghostnet.tzkt.io"
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -89,7 +87,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://ghostnet.tzkt.io"
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -98,9 +95,10 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
)}
</FormControl>

<FormControl>
<FormControl isInvalid={!!errors.buyTezUrl}>
<FormLabel>Buy Tez URL</FormLabel>
<Input placeholder="https://faucet.ghostnet.teztnets.com" {...register("buyTezUrl")} />
{errors.buyTezUrl && <FormErrorMessage>{errors.buyTezUrl.message}</FormErrorMessage>}
</FormControl>
<ModalFooter>
<Button width="100%" isDisabled={!isValid} onClick={() => {}} type="submit">
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@chakra-ui/theme-tools": "^2.2.3",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hookform/resolvers": "^3.9.0",
"@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-query": "^5.59.0",
"@taquito/beacon-wallet": "^20.0.1",
Expand Down
70 changes: 53 additions & 17 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ beforeEach(() => {

describe("<EditNetworkMenu />", () => {
describe("edit mode", () => {
const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc.com",
tzktApiUrl: "https://tzkt.com",
tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};

beforeEach(() => {
store.dispatch(networksActions.upsertNetwork(customNetwork));
});
Expand All @@ -27,14 +35,6 @@ describe("<EditNetworkMenu />", () => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu network={customNetwork} />, store);

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -45,7 +45,9 @@ describe("<EditNetworkMenu />", () => {
user.type(screen.getByLabelText("Tzkt Explorer URL"), updatedNetwork.tzktExplorerUrl)
);

expect(screen.getByText("Save changes")).toBeEnabled();
await waitFor(() => {
expect(screen.getByText("Save changes")).toBeEnabled();
});

await act(() => user.click(screen.getByText("Save changes")));
expect(store.getState().networks.available).toEqual([MAINNET, GHOSTNET, updatedNetwork]);
Expand All @@ -55,14 +57,6 @@ describe("<EditNetworkMenu />", () => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu network={customNetwork} />, store);

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -85,6 +79,48 @@ describe("<EditNetworkMenu />", () => {
});
});

describe("URL fields validation", () => {
const urlFields = [
{ label: "RPC URL", required: true },
{ label: "Tzkt API URL", required: true },
{ label: "Tzkt Explorer URL", required: true },
{ label: "Buy Tez URL", required: false },
];

it.each(urlFields)("validates $label field", async ({ label, required }) => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu />, store);

await user.type(screen.getByLabelText(label), "invalid-url");
await user.tab();

await waitFor(() => {
expect(screen.getByText(`Enter a valid ${label}`)).toBeVisible();
});

await user.clear(screen.getByLabelText(label));
await user.tab();

if (required) {
await waitFor(() => {
expect(screen.getByText(`${label} is required`)).toBeVisible();
});
} else {
await waitFor(() => {
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
}

await user.type(screen.getByLabelText(label), "https://valid-url.com");
await user.tab();

await waitFor(() => {
expect(screen.queryByText(`Enter a valid ${label}`)).not.toBeInTheDocument();
});
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
});

describe("create mode", () => {
describe("name field", () => {
it("validates uniqueness", async () => {
Expand Down
27 changes: 10 additions & 17 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from "@chakra-ui/react";
import { useDynamicDrawerContext } from "@umami/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { getNetworkValidationScheme, useDynamicDrawerContext } from "@umami/components";
import { networksActions, useAppDispatch, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
Expand All @@ -21,7 +22,11 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
formState: { errors, isValid },
register,
handleSubmit,
} = useForm<Network>({ mode: "onBlur", defaultValues: network });
} = useForm<Network>({
mode: "onBlur",
defaultValues: network,
resolver: zodResolver(getNetworkValidationScheme(availableNetworks, network)),
});

const onSubmit = (network: Network) => {
dispatch(networksActions.upsertNetwork(network));
Expand All @@ -35,17 +40,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
{!network && (
<FormControl isInvalid={!!errors.name}>
<FormLabel>Name</FormLabel>
<Input
placeholder="mainnet"
{...register("name", {
required: "Name is required",
validate: name => {
if (availableNetworks.find(n => n.name === name)) {
return "Network with this name already exists";
}
},
})}
/>
<Input placeholder="mainnet" {...register("name")} />
{errors.name && <FormErrorMessage>{errors.name.message}</FormErrorMessage>}
</FormControl>
)}
Expand All @@ -54,7 +49,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://prod.tcinfra.net/rpc/mainnet"
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -65,7 +59,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://api.ghostnet.tzkt.io"
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -76,7 +69,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://ghostnet.tzkt.io"
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -85,9 +77,10 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
)}
</FormControl>

<FormControl>
<FormControl isInvalid={!!errors.buyTezUrl}>
<FormLabel>Buy Tez URL</FormLabel>
<Input placeholder="https://faucet.ghostnet.teztnets.com" {...register("buyTezUrl")} />
{errors.buyTezUrl && <FormErrorMessage>{errors.buyTezUrl.message}</FormErrorMessage>}
</FormControl>
</VStack>
<Button width="100%" marginTop="30px" isDisabled={!isValid} type="submit" variant="primary">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GHOSTNET, formatPkh, mockImplicitAddress } from "@umami/tezos";

import { AddressPillText } from "./AddressPillText";
import { mockFA2AddressKind } from "./testUtils";
import { render, screen } from "../testUtils";
import { render, screen } from "../../testUtils";
const { upsert } = contactsActions;

let store: UmamiStore;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mockContractAddress } from "@umami/tezos";

import { type FA2Address } from "./types";
import { mockContractAddress } from "../../../tezos/src/testUtils";

export const mockFA2AddressKind = (index?: number): FA2Address => ({
type: "fa2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { cloneDeep } from "lodash";

import { useAddressKind } from "./useAddressKind";
import { renderHook } from "../testUtils";
import { renderHook } from "../../testUtils";

let store: UmamiStore;

Expand Down
Loading

1 comment on commit 8a9deb1

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.6% (1765/2111) 78.81% (837/1062) 78.14% (447/572)
apps/web Coverage: 83%
83.6% (1765/2111) 78.81% (837/1062) 78.14% (447/572)
packages/components Coverage: 97%
97.28% (179/184) 95.23% (80/84) 86.53% (45/52)
packages/core Coverage: 81%
82.22% (222/270) 71.73% (99/138) 81.96% (50/61)
packages/crypto Coverage: 100%
100% (28/28) 100% (3/3) 100% (5/5)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (35/35)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.61% (792/936) 80.97% (166/205) 78.72% (296/376)
packages/tezos Coverage: 86%
85.57% (89/104) 89.47% (17/19) 82.75% (24/29)
packages/tzkt Coverage: 86%
84.05% (58/69) 81.25% (13/16) 76.92% (30/39)

Please sign in to comment.