diff --git a/package.json b/package.json index 55f7b036..84269619 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "lodash.merge": "^4.6.2", "next": "13.5.3", "next-intl": "^2.20.0", - "next-safe-action": "^3.0.1", + "next-safe-action": "^3.4.0", "pg": "^8.11.1", "qrcode.react": "^3.1.0", "react": "18.2.0", @@ -158,8 +158,6 @@ }, "homepage": "https://github.com/meienberger/runtipi#readme", "pnpm": { - "patchedDependencies": { - "next-safe-action@3.0.1": "patches/next-safe-action@3.0.1.patch" - } + "patchedDependencies": {} } } diff --git a/patches/next-safe-action@3.0.1.patch b/patches/next-safe-action@3.0.1.patch deleted file mode 100644 index de7a0cb9..00000000 --- a/patches/next-safe-action@3.0.1.patch +++ /dev/null @@ -1,312 +0,0 @@ -diff --git a/dist/hook.d.ts b/dist/hook.d.ts -index 42220517df6bad298dd77f2e1961d6798ecfef0d..7b32f3634610ae20c0b108034a7da8048bc96205 100644 ---- a/dist/hook.d.ts -+++ b/dist/hook.d.ts -@@ -1,5 +1,9 @@ --import { z } from 'zod'; --import { C as ClientCaller, H as HookCallbacks, a as HookRes } from './types-31a698ec.js'; -+import { z } from "zod"; -+import { -+ C as ClientCaller, -+ H as HookCallbacks, -+ a as HookRes, -+} from "./types-31a698ec.js"; - - /** - * Use the action from a Client Component via hook. -@@ -8,14 +12,17 @@ import { C as ClientCaller, H as HookCallbacks, a as HookRes } from './types-31a - * - * {@link https://github.com/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action#2-the-hook-way See an example} - */ --declare const useAction: (clientCaller: ClientCaller, cb?: HookCallbacks | undefined) => { -- execute: (input: z.input) => void; -- isExecuting: boolean; -- res: HookRes; -- reset: () => void; -- hasExecuted: boolean; -- hasSucceded: boolean; -- hasErrored: boolean; -+declare const useAction: ( -+ clientCaller: ClientCaller, -+ cb?: HookCallbacks | undefined -+) => { -+ execute: (input: z.input) => void; -+ isExecuting: boolean; -+ res: HookRes; -+ reset: () => void; -+ hasExecuted: boolean; -+ hasSucceded: boolean; -+ hasErrored: boolean; - }; - /** - * Use the action from a Client Component via hook, with optimistic data update. -@@ -27,15 +34,22 @@ declare const useAction: (clientCalle - * - * {@link https://github.com/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action#optimistic-update--experimental See an example} - */ --declare const useOptimisticAction: (clientCaller: ClientCaller, initialOptData: Data, cb?: HookCallbacks | undefined) => { -- execute: (input: z.input, newOptimisticData: Partial) => Promise; -- isExecuting: boolean; -- res: HookRes; -- optimisticData: Data; -- reset: () => void; -- hasExecuted: boolean; -- hasSucceded: boolean; -- hasErrored: boolean; -+declare const useOptimisticAction: ( -+ clientCaller: ClientCaller, -+ initialOptData: Data, -+ cb?: HookCallbacks | undefined -+) => { -+ execute: ( -+ input: z.input, -+ newOptimisticData: Partial -+ ) => Promise; -+ isExecuting: boolean; -+ res: HookRes; -+ optimisticData: Data; -+ reset: () => void; -+ hasExecuted: boolean; -+ hasSucceded: boolean; -+ hasErrored: boolean; - }; - - export { HookCallbacks, HookRes, useAction, useOptimisticAction }; -diff --git a/dist/hook.mjs b/dist/hook.mjs -index 89d77b1da720c87508e525f09e2c0005a1986819..5b0d50450441734813511cce9c6df203104c6f66 100644 ---- a/dist/hook.mjs -+++ b/dist/hook.mjs -@@ -1,3 +1,5 @@ -+"use client"; -+ - // src/hook.ts - import { - experimental_useOptimistic, -@@ -5,31 +7,34 @@ import { - useEffect, - useRef, - useState, -- useTransition -+ useTransition, - } from "react"; - - // src/utils.ts --var isNextRedirectError = (e) => e instanceof Error && e.message === "NEXT_REDIRECT"; --var isNextNotFoundError = (e) => e instanceof Error && e.message === "NEXT_NOT_FOUND"; -+var isError = (e) => e instanceof Error; -+var isNextRedirectError = (e) => isError(e) && e.message === "NEXT_REDIRECT"; -+var isNextNotFoundError = (e) => isError(e) && e.message === "NEXT_NOT_FOUND"; - - // src/hook.ts --"use client"; - var getActionStatus = (res) => { - const hasSucceded = typeof res.data !== "undefined"; -- const hasErrored = typeof res.validationError !== "undefined" || typeof res.serverError !== "undefined" || typeof res.fetchError !== "undefined"; -+ const hasErrored = -+ typeof res.validationError !== "undefined" || -+ typeof res.serverError !== "undefined" || -+ typeof res.fetchError !== "undefined"; - const hasExecuted = hasSucceded || hasErrored; - return { hasExecuted, hasSucceded, hasErrored }; - }; --var useActionCallbacks = (res, hasSucceded, hasErrored, reset, cb) => { -+var useActionCallbacks = (input, res, hasSucceded, hasErrored, reset, cb) => { - const onSuccessRef = useRef(cb?.onSuccess); - const onErrorRef = useRef(cb?.onError); - useEffect(() => { - const onSuccess = onSuccessRef.current; - const onError = onErrorRef.current; - if (onSuccess && hasSucceded) { -- onSuccess(res.data, reset); -+ onSuccess(res.data, reset, input); - } else if (onError && hasErrored) { -- onError(res, reset); -+ onError(res, reset, input); - } - }, [hasErrored, hasSucceded, res, reset]); - }; -@@ -37,21 +42,31 @@ var useAction = (clientCaller, cb) => { - const [isExecuting, startTransition] = useTransition(); - const executor = useRef(clientCaller); - const [res, setRes] = useState({}); -+ const [input, setInput] = useState(); -+ const onExecuteRef = useRef(cb?.onExecute); - const { hasExecuted, hasSucceded, hasErrored } = getActionStatus(res); -- const execute = useCallback((input) => { -+ const execute = useCallback((input2) => { -+ setInput(input2); -+ const onExecute = onExecuteRef.current; -+ if (onExecute) { -+ onExecute(input2); -+ } - return startTransition(() => { -- return executor.current(input).then((res2) => setRes(res2)).catch((e) => { -- if (isNextRedirectError(e) || isNextNotFoundError(e)) { -- throw e; -- } -- setRes({ fetchError: e }); -- }); -+ return executor -+ .current(input2) -+ .then((res2) => setRes(res2)) -+ .catch((e) => { -+ if (isNextRedirectError(e) || isNextNotFoundError(e)) { -+ throw e; -+ } -+ setRes({ fetchError: e }); -+ }); - }); - }, []); - const reset = useCallback(() => { - setRes({}); - }, []); -- useActionCallbacks(res, hasSucceded, hasErrored, reset, cb); -+ useActionCallbacks(input, res, hasSucceded, hasErrored, reset, cb); - return { - execute, - isExecuting, -@@ -59,34 +74,47 @@ var useAction = (clientCaller, cb) => { - reset, - hasExecuted, - hasSucceded, -- hasErrored -+ hasErrored, - }; - }; - var useOptimisticAction = (clientCaller, initialOptData, cb) => { - const [res, setRes] = useState({}); -- const [optState, syncState] = experimental_useOptimistic({ ...initialOptData, ...res.data, __isExecuting__: false }, (state, newState) => ({ -- ...state, -- ...newState, -- __isExecuting__: true -- })); -+ const [input, setInput] = useState(); -+ const [optState, syncState] = experimental_useOptimistic( -+ { ...initialOptData, ...res.data, __isExecuting__: false }, -+ (state, newState) => ({ -+ ...state, -+ ...newState, -+ __isExecuting__: true, -+ }) -+ ); - const executor = useRef(clientCaller); -+ const onExecuteRef = useRef(cb?.onExecute); - const { hasExecuted, hasSucceded, hasErrored } = getActionStatus(res); - const execute = useCallback( -- (input, newOptimisticData) => { -+ (input2, newOptimisticData) => { - syncState(newOptimisticData); -- return executor.current(input).then((res2) => setRes(res2)).catch((e) => { -- if (isNextRedirectError(e) || isNextNotFoundError(e)) { -- throw e; -- } -- setRes({ fetchError: e }); -- }); -+ setInput(input2); -+ const onExecute = onExecuteRef.current; -+ if (onExecute) { -+ onExecute(input2); -+ } -+ return executor -+ .current(input2) -+ .then((res2) => setRes(res2)) -+ .catch((e) => { -+ if (isNextRedirectError(e) || isNextNotFoundError(e)) { -+ throw e; -+ } -+ setRes({ fetchError: e }); -+ }); - }, - [syncState] - ); - const reset = useCallback(() => { - setRes({}); - }, []); -- useActionCallbacks(res, hasSucceded, hasErrored, reset, cb); -+ useActionCallbacks(input, res, hasSucceded, hasErrored, reset, cb); - const { __isExecuting__, ...optimisticData } = optState; - return { - execute, -@@ -97,11 +125,8 @@ var useOptimisticAction = (clientCaller, initialOptData, cb) => { - reset, - hasExecuted, - hasSucceded, -- hasErrored -+ hasErrored, - }; - }; --export { -- useAction, -- useOptimisticAction --}; -+export { useAction, useOptimisticAction }; - //# sourceMappingURL=hook.mjs.map -diff --git a/dist/types-31a698ec.d.ts b/dist/types-31a698ec.d.ts -index c9339a0a530dd1825af0c6e1b4a6e9a58adc3c97..1eb8b8303d1534deee9f0084b6c62c9eabb147b2 100644 ---- a/dist/types-31a698ec.d.ts -+++ b/dist/types-31a698ec.d.ts -@@ -1,30 +1,53 @@ --import { z } from 'zod'; -+import { z } from "zod"; - - /** - * Type of the function called from Client Components with typesafe input data for the Server Action. - */ --type ClientCaller = (input: z.input) => Promise<{ -- data?: Data; -- serverError?: true; -- validationError?: Partial, string[]>>; -+type ClientCaller = ( -+ input: z.input -+) => Promise<{ -+ data?: Data; -+ serverError?: string; -+ validationError?: Partial, string[]>>; - }>; - /** - * Type of the function that executes server code when defining a new safe action. - */ --type ActionServerFn = (parsedInput: z.input, ctx: Context) => Promise; -+type ActionServerFn = ( -+ parsedInput: z.input, -+ ctx: Context -+) => Promise; - /** - * Type of `res` object returned by `useAction` and `useOptimisticAction` hooks. - */ --type HookRes = Awaited>> & { -- fetchError?: unknown; -+type HookRes = Awaited< -+ ReturnType> -+> & { -+ fetchError?: unknown; - }; - /** - * Type of hooks callbacks (`onSuccess` and `onError`). - * These are executed when the action succeeds or fails. - */ - type HookCallbacks = { -- onSuccess?: (data: NonNullable, "data">["data"]>, reset: () => void) => void; -- onError?: (error: Omit, "data">, reset: () => void) => void; -+ onSuccess?: ( -+ data: NonNullable, "data">["data"]>, -+ reset: () => void, -+ input: z.input -+ ) => void; -+ onError?: ( -+ error: Omit, "data">, -+ reset: () => void, -+ input: z.input -+ ) => void; -+ onExecute?: (input: z.input) => unknown; - }; -+type MaybePromise = T | Promise; - --export { ActionServerFn as A, ClientCaller as C, HookCallbacks as H, HookRes as a }; -+export { -+ ActionServerFn as A, -+ ClientCaller as C, -+ HookCallbacks as H, -+ MaybePromise as M, -+ HookRes as a, -+}; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7447a21..687c2b0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,10 +1,5 @@ lockfileVersion: '6.0' -patchedDependencies: - next-safe-action@3.0.1: - hash: i3wdronoj5npuplkzavpvyaj6e - path: patches/next-safe-action@3.0.1.patch - importers: .: @@ -100,8 +95,8 @@ importers: specifier: ^2.20.0 version: 2.20.0(next@13.5.3)(react@18.2.0) next-safe-action: - specifier: ^3.0.1 - version: 3.0.1(patch_hash=i3wdronoj5npuplkzavpvyaj6e)(next@13.5.3)(react@18.2.0)(zod@3.21.4) + specifier: ^3.4.0 + version: 3.4.0(next@13.5.3)(react@18.2.0)(zod@3.21.4) pg: specifier: ^8.11.1 version: 8.11.1 @@ -525,7 +520,7 @@ packages: '@babel/helper-validator-option': 7.22.5 browserslist: 4.21.5 lru-cache: 5.1.1 - semver: 6.3.0 + semver: 6.3.1 /@babel/helper-environment-visitor@7.22.5: resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} @@ -2055,7 +2050,7 @@ packages: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.5.3 + semver: 7.5.4 tar: 6.1.13 transitivePeerDependencies: - encoding @@ -3781,7 +3776,7 @@ packages: '@typescript-eslint/typescript-estree': 5.59.11(typescript@5.2.2) eslint: 8.50.0 eslint-scope: 5.1.1 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -5086,6 +5081,7 @@ packages: /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + requiresBuild: true dependencies: clone: 1.0.4 dev: true @@ -5644,7 +5640,7 @@ packages: eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-typescript@3.5.5)(eslint@8.50.0) object.assign: 4.1.4 object.entries: 1.1.6 - semver: 6.3.0 + semver: 6.3.1 dev: true /eslint-config-airbnb-typescript@17.0.0(@typescript-eslint/eslint-plugin@6.7.3)(@typescript-eslint/parser@6.7.3)(eslint-plugin-import@2.27.5)(eslint@8.50.0): @@ -6049,7 +6045,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.3.0(eslint@8.50.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) '@eslint-community/regexpp': 4.9.0 '@eslint/eslintrc': 2.1.2 '@eslint/js': 8.50.0 @@ -8918,8 +8914,8 @@ packages: react: 18.2.0 dev: true - /next-safe-action@3.0.1(patch_hash=i3wdronoj5npuplkzavpvyaj6e)(next@13.5.3)(react@18.2.0)(zod@3.21.4): - resolution: {integrity: sha512-qQOHz4Z1vnW9fKAl3+nmSoONtX8kvqJBJJ4PkRlkSF8AfFJnYp7PZ5qvtdIBTzxNoQLtM/CyVqlAM/6dCHJ62w==} + /next-safe-action@3.4.0(next@13.5.3)(react@18.2.0)(zod@3.21.4): + resolution: {integrity: sha512-EUmcChSlfIjdHu2n6L4w3EQnb1Np9C2OEWObPSKggdK/IhihDDatsunZKGcuimxFO3JbBdpFiUYcMe2slbovUg==} engines: {node: '>=16'} peerDependencies: next: '>= 13.4.2' @@ -8930,7 +8926,6 @@ packages: react: 18.2.0 zod: 3.21.4 dev: false - patched: true /next@13.5.3(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.63.6): resolution: {integrity: sha512-4Nt4HRLYDW/yRpJ/QR2t1v63UOMS55A38dnWv3UDOWGezuY0ZyFO1ABNbD7mulVzs9qVhgy2+ppjdsANpKP1mg==} @@ -10689,7 +10684,7 @@ packages: methods: 1.1.2 mime: 2.6.0 qs: 6.11.0 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color dev: true diff --git a/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx b/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx index 3852c08f..a415c571 100644 --- a/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx +++ b/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx @@ -1,55 +1,9 @@ import React from 'react'; -import { server } from '@/client/mocks/server'; -import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock'; import { faker } from '@faker-js/faker'; import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils'; import { ChangePasswordForm } from './ChangePasswordForm'; describe('', () => { - it('should show success toast upon password change', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true })); - render(); - const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' }); - const newPasswordInput = screen.getByRole('textbox', { name: 'newPassword' }); - const confirmPasswordInput = screen.getByRole('textbox', { name: 'newPasswordConfirm' }); - const newPassword = faker.string.alphanumeric(8); - - // act - fireEvent.change(currentPasswordInput, { target: { value: 'test' } }); - fireEvent.change(newPasswordInput, { target: { value: newPassword } }); - fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }); - const submitButton = screen.getByRole('button', { name: /Change password/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Password changed successfully')).toBeInTheDocument(); - }); - }); - - it('should show error toast if change password failed', async () => { - // arrange - server.use(getTRPCMockError({ path: ['auth', 'changePassword'], type: 'mutation', message: 'Invalid password' })); - render(); - const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' }); - const newPasswordInput = screen.getByRole('textbox', { name: 'newPassword' }); - const confirmPasswordInput = screen.getByRole('textbox', { name: 'newPasswordConfirm' }); - const newPassword = faker.string.alphanumeric(8); - - // act - fireEvent.change(currentPasswordInput, { target: { value: faker.string.alphanumeric(8) } }); - fireEvent.change(newPasswordInput, { target: { value: newPassword } }); - fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }); - const submitButton = screen.getByRole('button', { name: /Change password/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText(/Invalid password/)).toBeInTheDocument(); - }); - }); - it('should show error in the form if passwords do not match', async () => { // arrange render(); diff --git a/src/app/(dashboard)/settings/components/OtpForm/OptForm.test.tsx b/src/app/(dashboard)/settings/components/OtpForm/OptForm.test.tsx deleted file mode 100644 index 7e1ac493..00000000 --- a/src/app/(dashboard)/settings/components/OtpForm/OptForm.test.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import React from 'react'; -import { server } from '@/client/mocks/server'; -import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock'; -import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils'; -import { OtpForm } from './OtpForm'; - -describe('', () => { - it('should render', () => { - render(); - }); - - it('should prompt for password when enabling 2FA', async () => { - // arrange - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - }); - - it('should prompt for password when disabling 2FA', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'me'], response: { totpEnabled: true, id: 12, username: 'test', locale: 'en', operator: true } })); - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - }); - - it('should show show error toast if password is incorrect while enabling 2FA', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'me'], response: { totpEnabled: false, id: 12, username: 'test', locale: 'en', operator: true } })); - server.use(getTRPCMockError({ path: ['auth', 'getTotpUri'], type: 'mutation', message: 'Invalid password' })); - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Enable two-factor authentication/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText(/Invalid password/)).toBeInTheDocument(); - }); - }); - - it('should show show error toast if password is incorrect while disabling 2FA', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'me'], response: { locale: 'en', totpEnabled: true, id: 12, username: 'test', operator: true } })); - server.use(getTRPCMockError({ path: ['auth', 'disableTotp'], type: 'mutation', message: 'Invalid password' })); - render(); - - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Disable two-factor authentication/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText(/Invalid password/)).toBeInTheDocument(); - }); - }); - - it('should show success toast if password is correct while disabling 2FA', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'me'], response: { locale: 'en', totpEnabled: true, id: 12, username: 'test', operator: true } })); - server.use(getTRPCMock({ path: ['auth', 'disableTotp'], type: 'mutation', response: true })); - - render(); - - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Disable two-factor authentication/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Two-factor authentication disabled')).toBeInTheDocument(); - }); - }); - - it('should show secret key and QR code when enabling 2FA', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } })); - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Enable two-factor authentication/i }); - submitButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument(); - }); - expect(screen.getByRole('textbox', { name: 'secret key' })).toHaveValue('test'); - expect(screen.getByRole('button', { name: 'Enable two-factor authentication' })).toBeDisabled(); - }); - - it('should show error toast if submitted totp code is invalid', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } })); - server.use(getTRPCMockError({ path: ['auth', 'setupTotp'], type: 'mutation', message: 'Invalid code' })); - - render(); - - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Enable two-factor authentication/i }); - submitButton.click(); - - await waitFor(() => { - expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument(); - }); - - const inputEls = screen.getAllByRole('textbox', { name: /digit-/ }); - - inputEls.forEach((inputEl) => { - fireEvent.change(inputEl, { target: { value: '1' } }); - }); - - const enable2FAButton = screen.getByRole('button', { name: 'Enable two-factor authentication' }); - enable2FAButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText(/Invalid code/)).toBeInTheDocument(); - }); - }); - - it('should show success toast if submitted totp code is valid', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } })); - server.use(getTRPCMock({ path: ['auth', 'setupTotp'], type: 'mutation', response: true })); - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - const passwordInput = screen.getByRole('textbox', { name: 'password' }); - fireEvent.change(passwordInput, { target: { value: 'test' } }); - const submitButton = screen.getByRole('button', { name: /Enable two-factor authentication/i }); - submitButton.click(); - - await waitFor(() => { - expect(screen.getByText('Scan this QR code with your authenticator app.')).toBeInTheDocument(); - }); - - const inputEls = screen.getAllByRole('textbox', { name: /digit-/ }); - - inputEls.forEach((inputEl) => { - fireEvent.change(inputEl, { target: { value: '1' } }); - }); - - const enable2FAButton = screen.getByRole('button', { name: 'Enable two-factor authentication' }); - enable2FAButton.click(); - - // assert - await waitFor(() => { - expect(screen.getByText('Two-factor authentication enabled')).toBeInTheDocument(); - }); - }); - - it('can close the setup modal by clicking on the esc key', async () => { - // arrange - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - - fireEvent.keyDown(document, { key: 'Escape' }); - - // assert - await waitFor(() => { - expect(screen.queryByText('Password needed')).not.toBeInTheDocument(); - }); - }); - - it('can close the disable modal by clicking on the esc key', async () => { - // arrange - server.use(getTRPCMock({ path: ['auth', 'me'], response: { locale: 'en', totpEnabled: true, username: '', id: 1, operator: true } })); - render(); - const twoFactorAuthButton = screen.getByRole('switch', { name: /Enable two-factor authentication/i }); - await waitFor(() => { - expect(twoFactorAuthButton).toBeEnabled(); - }); - - // act - twoFactorAuthButton.click(); - await waitFor(() => { - expect(screen.getByText('Password needed')).toBeInTheDocument(); - }); - - fireEvent.keyDown(document, { key: 'Escape' }); - - // assert - await waitFor(() => { - expect(screen.queryByText('Password needed')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx b/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx index 8f5fd001..0f4f6984 100644 --- a/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx +++ b/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx @@ -50,7 +50,6 @@ export const OtpForm = (props: { totpEnabled: boolean }) => { setKey(''); setUri(''); toast.success(t('2fa-enable-success')); - // ctx.auth.me.invalidate(); } }, }); @@ -65,7 +64,6 @@ export const OtpForm = (props: { totpEnabled: boolean }) => { toast.error(data.failure.reason); } else { toast.success(t('2fa-disable-success')); - //ctx.auth.me.invalidate(); } }, }); diff --git a/src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.test.tsx b/src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.test.tsx index b2ed092d..4db338cd 100644 --- a/src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.test.tsx +++ b/src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.test.tsx @@ -4,6 +4,6 @@ import { SecurityContainer } from './SecurityContainer'; describe('', () => { it('should render', () => { - render(); + render(); }); }); diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index f5e87dc3..9a66a4a8 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -5,11 +5,11 @@ import React from 'react'; import { SystemServiceClass } from '@/server/services/system'; import { getSettings } from '@/server/core/TipiConfig'; import { getCurrentLocale } from 'src/utils/getCurrentLocale'; +import { getUserFromCookie } from '@/server/common/session.helpers'; import { SettingsTabTriggers } from './components/SettingsTabTriggers'; import { GeneralActions } from './components/GeneralActions'; import { SettingsContainer } from './components/SettingsContainer'; import { SecurityContainer } from './components/SecurityContainer'; -import { getUserFromCookie } from '@/server/common/session.helpers'; export async function generateMetadata(): Promise { const translator = await getTranslatorFromCookie(); @@ -29,7 +29,7 @@ export default async function SettingsPage({ searchParams }: { searchParams: { t return (
- +