chore: bump next-safe-action
This commit is contained in:
parent
1b434e7355
commit
fbefc31bd3
8 changed files with 16 additions and 668 deletions
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: <const IV extends z.ZodTypeAny, const Data>(clientCaller: ClientCaller<IV, Data>, cb?: HookCallbacks<IV, Data> | undefined) => {
|
||||
- execute: (input: z.input<IV>) => void;
|
||||
- isExecuting: boolean;
|
||||
- res: HookRes<IV, Data>;
|
||||
- reset: () => void;
|
||||
- hasExecuted: boolean;
|
||||
- hasSucceded: boolean;
|
||||
- hasErrored: boolean;
|
||||
+declare const useAction: <const IV extends z.ZodTypeAny, const Data>(
|
||||
+ clientCaller: ClientCaller<IV, Data>,
|
||||
+ cb?: HookCallbacks<IV, Data> | undefined
|
||||
+) => {
|
||||
+ execute: (input: z.input<IV>) => void;
|
||||
+ isExecuting: boolean;
|
||||
+ res: HookRes<IV, Data>;
|
||||
+ 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: <const IV extends z.ZodTypeAny, const Data>(clientCalle
|
||||
*
|
||||
* {@link https://github.com/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action#optimistic-update--experimental See an example}
|
||||
*/
|
||||
-declare const useOptimisticAction: <const IV extends z.ZodTypeAny, const Data>(clientCaller: ClientCaller<IV, Data>, initialOptData: Data, cb?: HookCallbacks<IV, Data> | undefined) => {
|
||||
- execute: (input: z.input<IV>, newOptimisticData: Partial<Data>) => Promise<void>;
|
||||
- isExecuting: boolean;
|
||||
- res: HookRes<IV, Data>;
|
||||
- optimisticData: Data;
|
||||
- reset: () => void;
|
||||
- hasExecuted: boolean;
|
||||
- hasSucceded: boolean;
|
||||
- hasErrored: boolean;
|
||||
+declare const useOptimisticAction: <const IV extends z.ZodTypeAny, const Data>(
|
||||
+ clientCaller: ClientCaller<IV, Data>,
|
||||
+ initialOptData: Data,
|
||||
+ cb?: HookCallbacks<IV, Data> | undefined
|
||||
+) => {
|
||||
+ execute: (
|
||||
+ input: z.input<IV>,
|
||||
+ newOptimisticData: Partial<Data>
|
||||
+ ) => Promise<void>;
|
||||
+ isExecuting: boolean;
|
||||
+ res: HookRes<IV, Data>;
|
||||
+ 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<IV extends z.ZodTypeAny, Data> = (input: z.input<IV>) => Promise<{
|
||||
- data?: Data;
|
||||
- serverError?: true;
|
||||
- validationError?: Partial<Record<keyof z.input<IV>, string[]>>;
|
||||
+type ClientCaller<IV extends z.ZodTypeAny, Data> = (
|
||||
+ input: z.input<IV>
|
||||
+) => Promise<{
|
||||
+ data?: Data;
|
||||
+ serverError?: string;
|
||||
+ validationError?: Partial<Record<keyof z.input<IV>, string[]>>;
|
||||
}>;
|
||||
/**
|
||||
* Type of the function that executes server code when defining a new safe action.
|
||||
*/
|
||||
-type ActionServerFn<IV extends z.ZodTypeAny, Data, Context extends object> = (parsedInput: z.input<IV>, ctx: Context) => Promise<Data>;
|
||||
+type ActionServerFn<IV extends z.ZodTypeAny, Data, Context extends object> = (
|
||||
+ parsedInput: z.input<IV>,
|
||||
+ ctx: Context
|
||||
+) => Promise<Data>;
|
||||
/**
|
||||
* Type of `res` object returned by `useAction` and `useOptimisticAction` hooks.
|
||||
*/
|
||||
-type HookRes<IV extends z.ZodTypeAny, Data> = Awaited<ReturnType<ClientCaller<IV, Data>>> & {
|
||||
- fetchError?: unknown;
|
||||
+type HookRes<IV extends z.ZodTypeAny, Data> = Awaited<
|
||||
+ ReturnType<ClientCaller<IV, Data>>
|
||||
+> & {
|
||||
+ fetchError?: unknown;
|
||||
};
|
||||
/**
|
||||
* Type of hooks callbacks (`onSuccess` and `onError`).
|
||||
* These are executed when the action succeeds or fails.
|
||||
*/
|
||||
type HookCallbacks<IV extends z.ZodTypeAny, Data> = {
|
||||
- onSuccess?: (data: NonNullable<Pick<HookRes<IV, Data>, "data">["data"]>, reset: () => void) => void;
|
||||
- onError?: (error: Omit<HookRes<IV, Data>, "data">, reset: () => void) => void;
|
||||
+ onSuccess?: (
|
||||
+ data: NonNullable<Pick<HookRes<IV, Data>, "data">["data"]>,
|
||||
+ reset: () => void,
|
||||
+ input: z.input<IV>
|
||||
+ ) => void;
|
||||
+ onError?: (
|
||||
+ error: Omit<HookRes<IV, Data>, "data">,
|
||||
+ reset: () => void,
|
||||
+ input: z.input<IV>
|
||||
+ ) => void;
|
||||
+ onExecute?: (input: z.input<IV>) => unknown;
|
||||
};
|
||||
+type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
-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,
|
||||
+};
|
|
@ -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
|
||||
|
|
|
@ -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('<ChangePasswordForm />', () => {
|
||||
it('should show success toast upon password change', async () => {
|
||||
// arrange
|
||||
server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true }));
|
||||
render(<ChangePasswordForm />);
|
||||
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(<ChangePasswordForm />);
|
||||
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(<ChangePasswordForm />);
|
||||
|
|
|
@ -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('<OtpForm />', () => {
|
||||
it('should render', () => {
|
||||
render(<OtpForm />);
|
||||
});
|
||||
|
||||
it('should prompt for password when enabling 2FA', async () => {
|
||||
// arrange
|
||||
render(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
|
||||
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(<OtpForm />);
|
||||
|
||||
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(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
|
||||
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(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
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(<OtpForm />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,6 +4,6 @@ import { SecurityContainer } from './SecurityContainer';
|
|||
|
||||
describe('<SecurityContainer />', () => {
|
||||
it('should render', () => {
|
||||
render(<SecurityContainer />);
|
||||
render(<SecurityContainer totpEnabled={false} />);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Metadata> {
|
||||
const translator = await getTranslatorFromCookie();
|
||||
|
@ -29,7 +29,7 @@ export default async function SettingsPage({ searchParams }: { searchParams: { t
|
|||
|
||||
return (
|
||||
<div className="card d-flex">
|
||||
<Tabs defaultValue={(tab as string) || 'actions'}>
|
||||
<Tabs defaultValue={tab || 'actions'}>
|
||||
<SettingsTabTriggers />
|
||||
<TabsContent value="actions">
|
||||
<GeneralActions version={version} />
|
||||
|
|
Loading…
Reference in a new issue