diff --git a/apps/desktop-e2e/src/features/multisig.feature b/apps/desktop-e2e/src/features/multisig.feature index f9635a8b1b..9c7c0c1eba 100644 --- a/apps/desktop-e2e/src/features/multisig.feature +++ b/apps/desktop-e2e/src/features/multisig.feature @@ -2,8 +2,8 @@ Feature: Multisig Account Creation Scenario: User creates a multisig account Given I have account - | type | label | secretKey | password | - | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | 12345678 | + | type | label | secretKey | password | + | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | Qwerty123123!23vcxz | And I am on an Accounts page When I am creating a multisig account @@ -25,7 +25,7 @@ Feature: Multisig Account Creation | Approvers | Alice,tz1gUNyn3hmnEWqkusWPzxRaon1cs7ndWh7h | | Min No. of approvals | 1 | - When I sign transaction with password "12345678" + When I sign transaction with password "Qwerty123123!23vcxz" Then I see "Operation Submitted" modal And I close modal diff --git a/apps/desktop-e2e/src/features/onboarding.feature b/apps/desktop-e2e/src/features/onboarding.feature index 6996e6fc81..3e522c7873 100644 --- a/apps/desktop-e2e/src/features/onboarding.feature +++ b/apps/desktop-e2e/src/features/onboarding.feature @@ -33,8 +33,8 @@ Feature: User Onboarding When I click "Continue" button Then I am on "Umami Master Password" onboarding page - When I fill "Password" with "12345678" - And I fill "Confirm Password" with "12345678" + When I fill "Password" with "Qwerty123123!23vcxz" + And I fill "Confirm Password" with "Qwerty123123!23vcxz" And I click "Submit" button Then I am on an Accounts page And I see a toast "Account successfully created!" @@ -76,8 +76,8 @@ Feature: User Onboarding When I click "Continue" button Then I am on "Umami Master Password" onboarding page - When I fill "Password" with "12345678" - And I fill "Confirm Password" with "12345678" + When I fill "Password" with "Qwerty123123!23vcxz" + And I fill "Confirm Password" with "Qwerty123123!23vcxz" And I click "Submit" button Then I am on an Accounts page And I see a toast "Account successfully created!" @@ -119,8 +119,8 @@ Feature: User Onboarding When I click "Continue" button Then I am on "Umami Master Password" onboarding page - When I fill "Password" with "12345678" - And I fill "Confirm Password" with "12345678" + When I fill "Password" with "Qwerty123123!23vcxz" + And I fill "Confirm Password" with "Qwerty123123!23vcxz" And I click "Submit" button Then I am on an Accounts page And I see a toast "Account successfully created!" @@ -158,8 +158,8 @@ Feature: User Onboarding When I click "Continue" button Then I am on "Umami Master Password" onboarding page - When I fill "Password" with "12345678" - And I fill "Confirm Password" with "12345678" + When I fill "Password" with "Qwerty123123!23vcxz" + And I fill "Confirm Password" with "Qwerty123123!23vcxz" And I click "Submit" button Then I am on an Accounts page And I see a toast "Account successfully created!" diff --git a/apps/desktop-e2e/src/features/staking.feature b/apps/desktop-e2e/src/features/staking.feature index 58d52701bf..1ca1877dd1 100644 --- a/apps/desktop-e2e/src/features/staking.feature +++ b/apps/desktop-e2e/src/features/staking.feature @@ -8,8 +8,8 @@ Feature: Staking # stake - unstake - finalize unstake Scenario: I delegate to a baker Given I have account - | type | label | secretKey | password | - | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | 12345678 | + | type | label | secretKey | password | + | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | Qwerty123123!23vcxz | And I am on an Accounts page When I open account drawer for "Alice" And I delegate to "baker1" @@ -23,8 +23,8 @@ Feature: Staking Scenario: I undelegate Given I have account - | type | label | secretKey | password | - | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | 12345678 | + | type | label | secretKey | password | + | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | Qwerty123123!23vcxz | And I am on an Accounts page When I open account drawer for "Alice" And I delegate to "baker1" @@ -44,8 +44,8 @@ Feature: Staking Scenario: I stake to a baker Given I have account - | type | label | secretKey | password | - | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | 12345678 | + | type | label | secretKey | password | + | secret_key | Alice | edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq | Qwerty123123!23vcxz | And I am on an Accounts page Given baker "baker1" sets delegation parameters | limit-of-staking-over-baking | 5 | diff --git a/apps/desktop-e2e/src/features/updates.feature b/apps/desktop-e2e/src/features/updates.feature index 122aeab07d..7193efbb90 100644 --- a/apps/desktop-e2e/src/features/updates.feature +++ b/apps/desktop-e2e/src/features/updates.feature @@ -2,8 +2,8 @@ Feature: Automatic updates Scenario: My account has been topped-up Given I have account - | type | label | password | secretKey | - | secret_key | Account 1 | 12345678 | spsk2jm29sHC99HDi64VBpSwEMZRQ7WfHdvQPVMZCkyWyR4spBrtRW | + | type | label | password | secretKey | + | secret_key | Account 1 | Qwerty123123!23vcxz | spsk2jm29sHC99HDi64VBpSwEMZRQ7WfHdvQPVMZCkyWyR4spBrtRW | And I am on an Accounts page When "Account 1" is topped-up with "1.123" And I wait until the next refetch diff --git a/apps/desktop-e2e/src/pages/AccountDrawerPage.ts b/apps/desktop-e2e/src/pages/AccountDrawerPage.ts index ddb3c18039..ebb26cfc7c 100644 --- a/apps/desktop-e2e/src/pages/AccountDrawerPage.ts +++ b/apps/desktop-e2e/src/pages/AccountDrawerPage.ts @@ -22,7 +22,7 @@ export class AccountDrawerPage { await this.page.getByRole("button", { name: "Preview" }).click(); - await new SignPage(this.page, "12345678").sign(); + await new SignPage(this.page, "Qwerty123123!23vcxz").sign(); } getRoundButton(buttonName: string): Locator { @@ -46,7 +46,7 @@ export class AccountDrawerPage { await this.page.getByRole("button", { name: "Preview" }).click(); - await new SignPage(this.page, "12345678").sign(); + await new SignPage(this.page, "Qwerty123123!23vcxz").sign(); } async stake(amount: number): Promise { @@ -59,7 +59,7 @@ export class AccountDrawerPage { await this.page.getByLabel("Enter Amount").fill(String(amount)); await this.page.getByRole("button", { name: "Preview" }).click(); - await new SignPage(this.page, "12345678").sign(); + await new SignPage(this.page, "Qwerty123123!23vcxz").sign(); } async stakedBalance(): Promise { diff --git a/apps/desktop/jest.config.ts b/apps/desktop/jest.config.ts index dba0ee3597..61c575109d 100644 --- a/apps/desktop/jest.config.ts +++ b/apps/desktop/jest.config.ts @@ -5,5 +5,5 @@ export default { rootDir: "./", testTimeout: 20000, bail: false, - setupFilesAfterEnv: ["/src/setupTests.ts"], + setupFilesAfterEnv: ["/src/setupTests.tsx"], }; diff --git a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigPendingOperation.test.tsx b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigPendingOperation.test.tsx index e881b19c21..dca66743a9 100644 --- a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigPendingOperation.test.tsx +++ b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigPendingOperation.test.tsx @@ -36,6 +36,7 @@ jest.mock("@umami/state", () => ({ })); const MOCK_TEZOS_TOOLKIT = {}; +const password = "Qwerty123123!23vcxz"; let store: UmamiStore; beforeEach(() => { @@ -124,7 +125,7 @@ describe("", () => { expect(jest.mocked(estimate)).toHaveBeenCalledWith(operation, MAINNET); - await act(() => user.type(screen.getByTestId("password"), "mockPass")); + await act(() => user.type(screen.getByTestId("password"), password)); const submitButton = screen.getByRole("button", { name: "Execute transaction", @@ -187,7 +188,7 @@ describe("", () => { expect(jest.mocked(estimate)).toHaveBeenCalledWith(operations, MAINNET); - await act(() => user.type(screen.getByTestId("password"), "mockPass")); + await act(() => user.type(screen.getByTestId("password"), password)); const submitButton = screen.getByRole("button", { name: "Approve transaction", diff --git a/apps/desktop/src/components/ChangePassword/ChangePasswordForm.test.tsx b/apps/desktop/src/components/ChangePassword/ChangePasswordForm.test.tsx index 3240dd112c..d9f239e32d 100644 --- a/apps/desktop/src/components/ChangePassword/ChangePasswordForm.test.tsx +++ b/apps/desktop/src/components/ChangePassword/ChangePasswordForm.test.tsx @@ -46,7 +46,7 @@ describe("ChangePassword Form", () => { }); }); - it("requires 8 characters", async () => { + it("requires 12 characters", async () => { render(fixture()); const newPasswordInput = screen.getByTestId("new-password"); @@ -55,7 +55,7 @@ describe("ChangePassword Form", () => { await waitFor(() => { expect(screen.getByTestId("new-password-error")).toHaveTextContent( - "Your password must be at least 8 characters long" + "Password must be at least 12 characters long" ); }); }); @@ -104,8 +104,8 @@ describe("ChangePassword Form", () => { const newPasswordConfirmationInput = screen.getByTestId("new-password-confirmation"); fireEvent.change(currentPasswordInput, { target: { value: "myOldPassword" } }); - fireEvent.change(newPasswordInput, { target: { value: "myNewPassword" } }); - fireEvent.change(newPasswordConfirmationInput, { target: { value: "myNewPassword" } }); + fireEvent.change(newPasswordInput, { target: { value: "myNewPassword123L!" } }); + fireEvent.change(newPasswordConfirmationInput, { target: { value: "myNewPassword123L!" } }); await waitFor(() => { expect(screen.getByRole("button", { name: "Update Password" })).toBeEnabled(); diff --git a/apps/desktop/src/components/ChangePassword/ChangePasswordForm.tsx b/apps/desktop/src/components/ChangePassword/ChangePasswordForm.tsx index b85e636939..34b99db8fb 100644 --- a/apps/desktop/src/components/ChangePassword/ChangePasswordForm.tsx +++ b/apps/desktop/src/components/ChangePassword/ChangePasswordForm.tsx @@ -25,7 +25,7 @@ type ChangePasswordFormValues = { export const ChangePasswordForm = () => { const { onClose } = useDynamicModalContext(); - const form = useForm({ mode: "onBlur" }); + const form = useForm({ mode: "all" }); const toast = useToast(); const dispatch = useAppDispatch(); const { handleAsyncAction, isLoading } = useAsyncActionHandler(); @@ -80,6 +80,7 @@ export const ChangePasswordForm = () => { ( - + {children} diff --git a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.test.tsx b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.test.tsx index e3bf641bbe..d08a988ba8 100644 --- a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.test.tsx +++ b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.test.tsx @@ -3,6 +3,8 @@ import { noop } from "lodash"; import { EnterAndConfirmPassword } from "./EnterAndConfirmPassword"; import { act, render, screen, userEvent } from "../../../../mocks/testUtils"; +const mockPassword = "Qwerty123123!23vcxz"; + const fixture = (isLoading: boolean) => ( ); @@ -24,17 +26,12 @@ describe("", () => { describe("Form", () => { test("Working verification", async () => { render(fixture(false)); - await checkPasswords("password", "password", true); + await checkPasswords(mockPassword, mockPassword, true); }); test("Not matching password", async () => { render(fixture(false)); - await checkPasswords("password", "password1", false); - }); - - test("Not meeting password policy", async () => { - render(fixture(false)); - await checkPasswords("tes", "tes", false); + await checkPasswords(mockPassword, "password1", false); }); test("Form is loading", () => { diff --git a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.tsx b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.tsx index be31299204..118ca06f61 100644 --- a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.tsx +++ b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterAndConfirmPassword.tsx @@ -19,7 +19,7 @@ export const EnterAndConfirmPassword = ({ }; const form = useForm({ - mode: "onBlur", + mode: "onChange", }); const { @@ -42,6 +42,7 @@ export const EnterAndConfirmPassword = ({ {errors.password && {errors.password.message}} diff --git a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterPassword.test.tsx b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterPassword.test.tsx index 8d7a81409e..7d6d17c547 100644 --- a/apps/desktop/src/components/Onboarding/masterPassword/password/EnterPassword.test.tsx +++ b/apps/desktop/src/components/Onboarding/masterPassword/password/EnterPassword.test.tsx @@ -3,6 +3,8 @@ import { noop } from "lodash"; import { EnterPassword } from "./EnterPassword"; import { act, render, screen, userEvent } from "../../../../mocks/testUtils"; +const mockPassword = "Qwerty123123!23vcxz"; + const fixture = (isLoading: boolean) => ; const checkPasswords = async (password: string, expected: boolean) => { @@ -19,12 +21,7 @@ describe("", () => { describe("Form", () => { test("Working verification", async () => { render(fixture(false)); - await checkPasswords("password", true); - }); - - test("Not meeting password policy", async () => { - render(fixture(false)); - await checkPasswords("tes", false); + await checkPasswords(mockPassword, true); }); test("Form is loading", () => { diff --git a/apps/desktop/src/components/Onboarding/restoreSecretKey/RestoreSecretKey.tsx b/apps/desktop/src/components/Onboarding/restoreSecretKey/RestoreSecretKey.tsx index 45d93f07c9..4ac2842376 100644 --- a/apps/desktop/src/components/Onboarding/restoreSecretKey/RestoreSecretKey.tsx +++ b/apps/desktop/src/components/Onboarding/restoreSecretKey/RestoreSecretKey.tsx @@ -61,7 +61,7 @@ export const RestoreSecretKey = ({ goToStep }: { goToStep: (step: OnboardingStep {isEncrypted && ( - + {errors.password && {errors.password.message}} )} diff --git a/apps/desktop/src/components/PasswordInput.tsx b/apps/desktop/src/components/PasswordInput.tsx index c6804bee13..63f1a0843a 100644 --- a/apps/desktop/src/components/PasswordInput.tsx +++ b/apps/desktop/src/components/PasswordInput.tsx @@ -6,12 +6,12 @@ import { type InputProps, InputRightElement, } from "@chakra-ui/react"; +import { DEFAULT_MIN_LENGTH, usePasswordValidation } from "@umami/components"; import { useState } from "react"; import { type FieldValues, type Path, type RegisterOptions, useFormContext } from "react-hook-form"; import { EyeIcon, EyeSlashIcon } from "../assets/icons"; - -const MIN_LENGTH = 8; +import colors from "../style/colors"; // is needed to be compatible with the useForm's type parameter (FormData) // > makes sure that we can pass in only valid inputName that exists in FormData @@ -19,9 +19,10 @@ type PasswordInputProps> = { inputName: U; label?: string; placeholder?: string; + isStrengthCheckEnabled?: boolean; required?: string | boolean; minLength?: RegisterOptions["minLength"]; - validate?: RegisterOptions["validate"]; + validate?: (val: string) => string | boolean; } & InputProps; // TODO: add an error message and make it nested under FormControl @@ -30,12 +31,32 @@ export const PasswordInput = >({ label = "Password", placeholder = "Enter your password", required = "Password is required", - minLength = MIN_LENGTH, + isStrengthCheckEnabled = false, + minLength = DEFAULT_MIN_LENGTH, validate, ...rest }: PasswordInputProps) => { const { register } = useFormContext(); const [showPassword, setShowPassword] = useState(false); + const { validatePasswordStrength, PasswordStrengthBar } = usePasswordValidation({ + color: colors.gray[500], + inputName, + minLength, + }); + + const handleValidate = (val: string) => { + if (validate) { + const validationResult = validate(val); + + if (isStrengthCheckEnabled && validationResult === true) { + return validatePasswordStrength(val); + } + + return validationResult; + } else if (isStrengthCheckEnabled) { + return validatePasswordStrength(val); + } + }; return ( <> @@ -48,14 +69,7 @@ export const PasswordInput = >({ type={showPassword ? "text" : "password"} {...register(inputName, { required, - minLength: - minLength && required - ? { - value: minLength, - message: `Your password must be at least ${minLength} characters long`, - } - : undefined, - validate, + validate: handleValidate, })} {...rest} /> @@ -69,6 +83,7 @@ export const PasswordInput = >({ + {isStrengthCheckEnabled && PasswordStrengthBar} ); }; diff --git a/apps/desktop/src/setupTests.ts b/apps/desktop/src/setupTests.tsx similarity index 100% rename from apps/desktop/src/setupTests.ts rename to apps/desktop/src/setupTests.tsx diff --git a/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx index adb9ed3480..ea8f64642c 100644 --- a/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx +++ b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx @@ -52,7 +52,6 @@ export const MasterPasswordModal = ({ onSubmit }: MasterPasswordModalProps) => { inputName="password" label="Password" placeholder="Enter your password" - required="Password is required" /> diff --git a/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.test.tsx b/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.test.tsx index 6581a5a946..cc54cdd235 100644 --- a/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.test.tsx +++ b/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.test.tsx @@ -9,6 +9,9 @@ jest.mock("@umami/state", () => ({ changeMnemonicPassword: jest.fn(), })); +const currentPassword = "Qwerty123123!23vcxz"; +const newPassword = "Qwerty123123!23vcxz32llll"; + describe("", () => { describe("currentPassword", () => { it("is empty by default", async () => { @@ -47,7 +50,7 @@ describe("", () => { }); }); - it("requires 8 characters", async () => { + it("requires 12 characters", async () => { const user = userEvent.setup(); await renderInDrawer(); @@ -57,7 +60,7 @@ describe("", () => { await waitFor(() => { expect(screen.getByTestId("new-password-error")).toHaveTextContent( - "Your password must be at least 8 characters long" + "Password must be at least 12 characters long" ); }); }); @@ -108,9 +111,9 @@ describe("", () => { const newPasswordInput = screen.getByTestId("new-password"); const newPasswordConfirmationInput = screen.getByTestId("new-password-confirmation"); - await user.type(currentPasswordInput, "myOldPassword"); - await user.type(newPasswordInput, "myNewPassword"); - await user.type(newPasswordConfirmationInput, "myNewPassword"); + await user.type(currentPasswordInput, currentPassword); + await user.type(newPasswordInput, newPassword); + await user.type(newPasswordConfirmationInput, newPassword); expect(screen.getByRole("button", { name: "Update Password" })).toBeEnabled(); }); @@ -123,15 +126,15 @@ describe("", () => { const newPasswordInput = screen.getByTestId("new-password"); const newPasswordConfirmationInput = screen.getByTestId("new-password-confirmation"); - await user.type(currentPasswordInput, "myOldPassword"); - await user.type(newPasswordInput, "myNewPassword"); - await user.type(newPasswordConfirmationInput, "myNewPassword"); + await user.type(currentPasswordInput, currentPassword); + await user.type(newPasswordInput, newPassword); + await user.type(newPasswordConfirmationInput, newPassword); await user.click(screen.getByRole("button", { name: "Update Password" })); expect(changeMnemonicPassword).toHaveBeenCalledWith({ - currentPassword: "myOldPassword", - newPassword: "myNewPassword", + currentPassword: currentPassword, + newPassword: newPassword, }); }); }); diff --git a/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.tsx b/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.tsx index 92a1ade60b..38d925a324 100644 --- a/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.tsx +++ b/apps/web/src/components/Menu/ChangePasswordMenu/ChangePasswordMenu.tsx @@ -12,7 +12,7 @@ type ChangePasswordMenuValues = { }; export const ChangePasswordMenu = () => { - const form = useForm({ mode: "onBlur" }); + const form = useForm({ mode: "all" }); const toast = useToast(); const dispatch = useAppDispatch(); const { handleAsyncAction, isLoading } = useAsyncActionHandler(); @@ -44,6 +44,7 @@ export const ChangePasswordMenu = () => { ({ let store: UmamiStore; const account = mockImplicitAccount(0); +const password = "Qwerty123123!23vcxz"; beforeEach(() => { store = makeStore(); @@ -96,8 +97,8 @@ describe("", () => { await user.click(screen.getByText("Save Backup")); - await user.type(screen.getByLabelText("Set Password"), "password"); - await user.type(screen.getByLabelText("Confirm Password"), "password"); + await user.type(screen.getByLabelText("Set Password"), password); + await user.type(screen.getByLabelText("Confirm Password"), password); await user.click(screen.getByRole("button", { name: "Save Backup" })); expect(mockDownloadBackupFile).toHaveBeenCalled(); diff --git a/apps/web/src/components/Onboarding/ImportWallet/ImportBackupTab.tsx b/apps/web/src/components/Onboarding/ImportWallet/ImportBackupTab.tsx index 3543155762..ad8f23e727 100644 --- a/apps/web/src/components/Onboarding/ImportWallet/ImportBackupTab.tsx +++ b/apps/web/src/components/Onboarding/ImportWallet/ImportBackupTab.tsx @@ -106,7 +106,7 @@ export const ImportBackupTab = () => { /> {errors.file && {errors.file.message}} - + diff --git a/apps/web/src/components/Onboarding/ImportWallet/SecretKeyTab.tsx b/apps/web/src/components/Onboarding/ImportWallet/SecretKeyTab.tsx index f8fa7e2347..8d8f31d7e5 100644 --- a/apps/web/src/components/Onboarding/ImportWallet/SecretKeyTab.tsx +++ b/apps/web/src/components/Onboarding/ImportWallet/SecretKeyTab.tsx @@ -52,7 +52,7 @@ export const SecretKeyTab = () => { {errors.secretKey && {errors.secretKey.message}} - {isEncrypted && } + {isEncrypted && }