From 4fdf9b00eab7f95ce1f03053ef2837001e6026e5 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Sun, 3 Nov 2024 23:42:59 +0300 Subject: [PATCH] feat(useSignOutMutation): add useSignOutMutation hook --- packages/react/src/auth/index.ts | 2 +- .../src/auth/useSignOutMutation.test.tsx | 134 ++++++++++++++++++ packages/react/src/auth/useSignOutMutation.ts | 18 +++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/auth/useSignOutMutation.test.tsx create mode 100644 packages/react/src/auth/useSignOutMutation.ts diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 24520331..1e7446f4 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -26,7 +26,7 @@ export { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; // useSignInWithPhoneNumberMutation // useSignInWithPopupMutation // useSignInWithRedirectMutation -// useSignOutMutation +export { useSignOutMutation } from "./useSignOutMutation"; // useUpdateCurrentUserMutation // useValidatePasswordMutation // useVerifyPasswordResetCodeMutation diff --git a/packages/react/src/auth/useSignOutMutation.test.tsx b/packages/react/src/auth/useSignOutMutation.test.tsx new file mode 100644 index 00000000..e7b085e4 --- /dev/null +++ b/packages/react/src/auth/useSignOutMutation.test.tsx @@ -0,0 +1,134 @@ +import React from "react"; +import { describe, expect, test, beforeEach, vi } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useSignOutMutation } from "./useSignOutMutation"; +import { auth, wipeAuth } from "~/testing-utils"; +import { + createUserWithEmailAndPassword, + signInWithEmailAndPassword, +} from "firebase/auth"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, +}); + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe("useSignOutMutation", () => { + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); + + test("successfully signs out an authenticated user", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + + await act(async () => { + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser).toBeNull(); + }); + + test("handles sign out for a non-authenticated user", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + await auth.signOut(); + + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + + await act(async () => { + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser).toBeNull(); + }); + + test("calls onSuccess callback after successful sign out", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const onSuccessMock = vi.fn(); + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + const { result } = renderHook( + () => useSignOutMutation(auth, { onSuccess: onSuccessMock }), + { wrapper } + ); + + await act(async () => { + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + }); + + test("calls onError callback on sign out failure", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + const onErrorMock = vi.fn(); + const error = new Error("Sign out failed"); + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + const mockSignOut = vi.spyOn(auth, "signOut").mockRejectedValueOnce(error); + + const { result } = renderHook( + () => useSignOutMutation(auth, { onError: onErrorMock }), + { wrapper } + ); + + await act(async () => result.current.mutate()); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(onErrorMock).toHaveBeenCalled(); + expect(result.current.error).toBe(error); + expect(result.current.isSuccess).toBe(false); + mockSignOut.mockRestore(); + }); + + test("handles concurrent sign out attempts", async () => { + const email = "tqf@invertase.io"; + const password = "tanstackQueryFirebase#123"; + + await createUserWithEmailAndPassword(auth, email, password); + await signInWithEmailAndPassword(auth, email, password); + + const { result } = renderHook(() => useSignOutMutation(auth), { wrapper }); + + await act(async () => { + // Attempt multiple concurrent sign-outs + result.current.mutate(); + result.current.mutate(); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(auth.currentUser).toBeNull(); + }); +}); diff --git a/packages/react/src/auth/useSignOutMutation.ts b/packages/react/src/auth/useSignOutMutation.ts new file mode 100644 index 00000000..6ef0842e --- /dev/null +++ b/packages/react/src/auth/useSignOutMutation.ts @@ -0,0 +1,18 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query"; +import { type Auth, signOut } from "firebase/auth"; + +type AuthUseMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useSignOutMutation( + auth: Auth, + options?: AuthUseMutationOptions +) { + return useMutation({ + ...options, + mutationFn: () => signOut(auth), + }); +}