From 0ad449eff9ce3690c33266fd678db67394c2e9c7 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Sun, 26 Jan 2025 20:35:00 +0300 Subject: [PATCH] feat(react/auth): add useConfirmPasswordResetMutation --- packages/react/src/auth/index.ts | 2 +- .../useConfirmPasswordResetMutation.test.tsx | 119 ++++++++++++++++++ .../auth/useConfirmPasswordResetMutation.ts | 26 ++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/auth/useConfirmPasswordResetMutation.test.tsx create mode 100644 packages/react/src/auth/useConfirmPasswordResetMutation.ts diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 024d9a3..bbc9ce7 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -11,7 +11,7 @@ // useMultiFactorResolverResolveSignInMutation (MultiFactorResolver) // useApplyActionCodeMutation // useCheckActionCodeMutation -// useConfirmPasswordResetMutation +export { useConfirmPasswordResetMutation } from "./useConfirmPasswordResetMutation"; // useCreateUserWithEmailAndPasswordMutation // useFetchSignInMethodsForEmailQuery // useGetRedirectResultQuery diff --git a/packages/react/src/auth/useConfirmPasswordResetMutation.test.tsx b/packages/react/src/auth/useConfirmPasswordResetMutation.test.tsx new file mode 100644 index 0000000..aa2484f --- /dev/null +++ b/packages/react/src/auth/useConfirmPasswordResetMutation.test.tsx @@ -0,0 +1,119 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { + createUserWithEmailAndPassword, + sendPasswordResetEmail, +} from "firebase/auth"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils"; +import { useConfirmPasswordResetMutation } from "./useConfirmPasswordResetMutation"; +import { waitForPasswordResetCode } from "./utils"; +import { queryClient, wrapper } from "../../utils"; + +describe("useConfirmPasswordResetMutation", () => { + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; + const newPassword = "NewSecurePassword#456"; + + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + await createUserWithEmailAndPassword(auth, email, password); + }); + + afterEach(async () => { + vi.clearAllMocks(); + await auth.signOut(); + }); + + test("successfully resets password", async () => { + await sendPasswordResetEmail(auth, email); + const oobCode = await waitForPasswordResetCode(email); + + const { result } = renderHook(() => useConfirmPasswordResetMutation(auth), { + wrapper, + }); + + await act(async () => { + await result.current.mutateAsync({ oobCode: oobCode!, newPassword }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); + + test("handles invalid action code", async () => { + const invalidCode = "invalid-action-code"; + + const { result } = renderHook(() => useConfirmPasswordResetMutation(auth), { + wrapper, + }); + + await act(async () => { + try { + await result.current.mutateAsync({ oobCode: invalidCode, newPassword }); + } catch (error) { + expectFirebaseError(error, "auth/invalid-action-code"); + } + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error).toBeDefined(); + expectFirebaseError(result.current.error, "auth/invalid-action-code"); + }); + + test("handles empty action code", async () => { + const { result } = renderHook(() => useConfirmPasswordResetMutation(auth), { + wrapper, + }); + + await act(async () => { + try { + await result.current.mutateAsync({ oobCode: "", newPassword }); + } catch (error) { + expectFirebaseError(error, "auth/internal-error"); + } + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(result.current.error).toBeDefined(); + expectFirebaseError(result.current.error, "auth/internal-error"); + }); + + test("executes onSuccess callback", async () => { + await sendPasswordResetEmail(auth, email); + const oobCode = await waitForPasswordResetCode(email); + const onSuccess = vi.fn(); + + const { result } = renderHook( + () => useConfirmPasswordResetMutation(auth, { onSuccess }), + { wrapper } + ); + + await act(async () => { + await result.current.mutateAsync({ oobCode: oobCode!, newPassword }); + }); + + await waitFor(() => expect(onSuccess).toHaveBeenCalled()); + }); + + test("executes onError callback", async () => { + const invalidCode = "invalid-action-code"; + const onError = vi.fn(); + + const { result } = renderHook( + () => useConfirmPasswordResetMutation(auth, { onError }), + { wrapper } + ); + + await act(async () => { + try { + await result.current.mutateAsync({ oobCode: invalidCode, newPassword }); + } catch (error) { + expectFirebaseError(error, "auth/invalid-action-code"); + } + }); + + await waitFor(() => expect(onError).toHaveBeenCalled()); + expect(onError.mock.calls[0][0]).toBeDefined(); + expectFirebaseError(result.current.error, "auth/invalid-action-code"); + }); +}); diff --git a/packages/react/src/auth/useConfirmPasswordResetMutation.ts b/packages/react/src/auth/useConfirmPasswordResetMutation.ts new file mode 100644 index 0000000..462e040 --- /dev/null +++ b/packages/react/src/auth/useConfirmPasswordResetMutation.ts @@ -0,0 +1,26 @@ +import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; +import { type Auth, type AuthError, confirmPasswordReset } from "firebase/auth"; + +type AuthUseMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useConfirmPasswordResetMutation( + auth: Auth, + options?: AuthUseMutationOptions< + void, + AuthError, + { oobCode: string; newPassword: string } + > +) { + return useMutation( + { + ...options, + mutationFn: ({ oobCode, newPassword }) => { + return confirmPasswordReset(auth, oobCode, newPassword); + }, + } + ); +}