Skip to content

Commit b424dd1

Browse files
authored
Merge pull request #121 from HassanBahati/ft-add-useSignOutMutation
feat: add useSignOutMutation hook
2 parents 86e04a1 + 6f32041 commit b424dd1

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed

packages/react/src/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export { useSignInWithEmailAndPasswordMutation } from "./useSignInWithEmailAndPa
2626
// useSignInWithPhoneNumberMutation
2727
// useSignInWithPopupMutation
2828
// useSignInWithRedirectMutation
29+
export { useSignOutMutation } from "./useSignOutMutation";
30+
// useUpdateCurrentUserMutation
2931
// useSignOutMutation
3032
export { useUpdateCurrentUserMutation } from "./useUpdateCurrentUserMutation";
3133
// useValidatePasswordMutation
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React from "react";
2+
import { describe, expect, test, beforeEach, vi } from "vitest";
3+
import { renderHook, act, waitFor } from "@testing-library/react";
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5+
import { useSignOutMutation } from "./useSignOutMutation";
6+
import { auth, wipeAuth } from "~/testing-utils";
7+
import {
8+
createUserWithEmailAndPassword,
9+
signInWithEmailAndPassword,
10+
} from "firebase/auth";
11+
12+
const queryClient = new QueryClient({
13+
defaultOptions: {
14+
queries: { retry: false },
15+
mutations: { retry: false },
16+
},
17+
});
18+
19+
const wrapper = ({ children }: { children: React.ReactNode }) => (
20+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
21+
);
22+
23+
describe("useSignOutMutation", () => {
24+
beforeEach(async () => {
25+
queryClient.clear();
26+
await wipeAuth();
27+
});
28+
29+
test("successfully signs out an authenticated user", async () => {
30+
const email = "[email protected]";
31+
const password = "tanstackQueryFirebase#123";
32+
33+
await createUserWithEmailAndPassword(auth, email, password);
34+
await signInWithEmailAndPassword(auth, email, password);
35+
36+
const { result } = renderHook(() => useSignOutMutation(auth), { wrapper });
37+
38+
await act(async () => {
39+
result.current.mutate();
40+
});
41+
42+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
43+
44+
expect(auth.currentUser).toBeNull();
45+
});
46+
47+
test("handles sign out for a non-authenticated user", async () => {
48+
const email = "[email protected]";
49+
const password = "tanstackQueryFirebase#123";
50+
51+
await createUserWithEmailAndPassword(auth, email, password);
52+
await signInWithEmailAndPassword(auth, email, password);
53+
54+
await auth.signOut();
55+
56+
const { result } = renderHook(() => useSignOutMutation(auth), { wrapper });
57+
58+
await act(async () => {
59+
result.current.mutate();
60+
});
61+
62+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
63+
64+
expect(auth.currentUser).toBeNull();
65+
});
66+
67+
test("calls onSuccess callback after successful sign out", async () => {
68+
const email = "[email protected]";
69+
const password = "tanstackQueryFirebase#123";
70+
const onSuccessMock = vi.fn();
71+
72+
await createUserWithEmailAndPassword(auth, email, password);
73+
await signInWithEmailAndPassword(auth, email, password);
74+
75+
const { result } = renderHook(
76+
() => useSignOutMutation(auth, { onSuccess: onSuccessMock }),
77+
{ wrapper }
78+
);
79+
80+
await act(async () => {
81+
result.current.mutate();
82+
});
83+
84+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
85+
86+
expect(onSuccessMock).toHaveBeenCalled();
87+
});
88+
89+
test("calls onError callback on sign out failure", async () => {
90+
const email = "[email protected]";
91+
const password = "tanstackQueryFirebase#123";
92+
const onErrorMock = vi.fn();
93+
const error = new Error("Sign out failed");
94+
95+
await createUserWithEmailAndPassword(auth, email, password);
96+
await signInWithEmailAndPassword(auth, email, password);
97+
98+
const mockSignOut = vi.spyOn(auth, "signOut").mockRejectedValueOnce(error);
99+
100+
const { result } = renderHook(
101+
() => useSignOutMutation(auth, { onError: onErrorMock }),
102+
{ wrapper }
103+
);
104+
105+
await act(async () => result.current.mutate());
106+
107+
await waitFor(() => expect(result.current.isError).toBe(true));
108+
109+
expect(onErrorMock).toHaveBeenCalled();
110+
expect(result.current.error).toBe(error);
111+
expect(result.current.isSuccess).toBe(false);
112+
mockSignOut.mockRestore();
113+
});
114+
115+
test("handles concurrent sign out attempts", async () => {
116+
const email = "[email protected]";
117+
const password = "tanstackQueryFirebase#123";
118+
119+
await createUserWithEmailAndPassword(auth, email, password);
120+
await signInWithEmailAndPassword(auth, email, password);
121+
122+
const { result } = renderHook(() => useSignOutMutation(auth), { wrapper });
123+
124+
await act(async () => {
125+
// Attempt multiple concurrent sign-outs
126+
result.current.mutate();
127+
result.current.mutate();
128+
});
129+
130+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
131+
132+
expect(auth.currentUser).toBeNull();
133+
});
134+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
2+
import { type Auth, signOut } from "firebase/auth";
3+
4+
type AuthUseMutationOptions<
5+
TData = unknown,
6+
TError = Error,
7+
TVariables = void
8+
> = Omit<UseMutationOptions<TData, TError, TVariables>, "mutationFn">;
9+
10+
export function useSignOutMutation(
11+
auth: Auth,
12+
options?: AuthUseMutationOptions
13+
) {
14+
return useMutation<void>({
15+
...options,
16+
mutationFn: () => signOut(auth),
17+
});
18+
}

0 commit comments

Comments
 (0)