Skip to content

Commit 61d235d

Browse files
HassanBahatiEhesp
andauthored
feat(react): add useRevokeAccessTokenMutation (#145)
Co-authored-by: Elliot Hesp <[email protected]>
1 parent a4fcff1 commit 61d235d

File tree

5 files changed

+237
-10
lines changed

5 files changed

+237
-10
lines changed

packages/react/src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export { useApplyActionCodeMutation } from "./useApplyActionCodeMutation";
1616
export { useConfirmPasswordResetMutation } from "./useConfirmPasswordResetMutation";
1717
// useCreateUserWithEmailAndPasswordMutation
1818
// useGetRedirectResultQuery
19+
export { useRevokeAccessTokenMutation } from "./useRevokeAccessTokenMutation";
1920
export { useGetRedirectResultQuery } from "./useGetRedirectResultQuery";
2021
// useRevokeAccessTokenMutation
2122
// useSendPasswordResetEmailMutation

packages/react/src/auth/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { UseMutationOptions } from "@tanstack/react-query";
2+
3+
export type AuthMutationOptions<
4+
TData = unknown,
5+
TError = Error,
6+
TVariables = void
7+
> = Omit<UseMutationOptions<TData, TError, TVariables>, "mutationFn">;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { useRevokeAccessTokenMutation } from "./useRevokeAccessTokenMutation";
2+
import { Auth, revokeAccessToken } from "firebase/auth";
3+
import { describe, expect, test, vi, beforeEach } from "vitest";
4+
import { act, renderHook, waitFor } from "@testing-library/react";
5+
import { wrapper, queryClient, expectInitialMutationState } from "../../utils";
6+
7+
vi.mock("firebase/auth", () => ({
8+
...vi.importActual("firebase/auth"),
9+
revokeAccessToken: vi.fn(),
10+
}));
11+
12+
describe("useRevokeAccessTokenMutation", () => {
13+
const mockAuth = {} as Auth;
14+
const mockToken = "mock-access-token";
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
queryClient.clear();
19+
});
20+
21+
test("should successfully revoke access token", async () => {
22+
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);
23+
24+
const { result } = renderHook(
25+
() => useRevokeAccessTokenMutation(mockAuth),
26+
{
27+
wrapper,
28+
}
29+
);
30+
31+
act(() => {
32+
result.current.mutate(mockToken);
33+
});
34+
35+
await waitFor(() => {
36+
expect(result.current.isSuccess).toBe(true);
37+
});
38+
39+
expect(revokeAccessToken).toHaveBeenCalledTimes(1);
40+
expect(revokeAccessToken).toHaveBeenCalledWith(mockAuth, mockToken);
41+
});
42+
43+
test("should handle revocation failure", async () => {
44+
const mockError = new Error("Failed to revoke token");
45+
vi.mocked(revokeAccessToken).mockRejectedValueOnce(mockError);
46+
47+
const { result } = renderHook(
48+
() => useRevokeAccessTokenMutation(mockAuth),
49+
{
50+
wrapper,
51+
}
52+
);
53+
54+
act(() => {
55+
result.current.mutate(mockToken);
56+
});
57+
58+
await waitFor(() => {
59+
expect(result.current.isError).toBe(true);
60+
});
61+
62+
expect(result.current.error).toBe(mockError);
63+
expect(revokeAccessToken).toHaveBeenCalledTimes(1);
64+
expect(revokeAccessToken).toHaveBeenCalledWith(mockAuth, mockToken);
65+
});
66+
67+
test("should accept and use custom mutation options", async () => {
68+
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);
69+
70+
const onSuccessMock = vi.fn();
71+
const onErrorMock = vi.fn();
72+
73+
const { result } = renderHook(
74+
() =>
75+
useRevokeAccessTokenMutation(mockAuth, {
76+
onSuccess: onSuccessMock,
77+
onError: onErrorMock,
78+
}),
79+
{
80+
wrapper,
81+
}
82+
);
83+
84+
act(() => {
85+
result.current.mutate(mockToken);
86+
});
87+
88+
await waitFor(() => {
89+
expect(result.current.isSuccess).toBe(true);
90+
});
91+
92+
expect(onSuccessMock).toHaveBeenCalledTimes(1);
93+
expect(onErrorMock).not.toHaveBeenCalled();
94+
});
95+
96+
test("should properly handle loading state throughout mutation lifecycle", async () => {
97+
vi.mocked(revokeAccessToken).mockImplementation(
98+
() => new Promise((resolve) => setTimeout(resolve, 100))
99+
);
100+
101+
const { result } = renderHook(
102+
() => useRevokeAccessTokenMutation(mockAuth),
103+
{
104+
wrapper,
105+
}
106+
);
107+
108+
expect(result.current.isPending).toBe(false);
109+
expect(result.current.isIdle).toBe(true);
110+
111+
await act(async () => {
112+
await result.current.mutateAsync(mockToken);
113+
});
114+
115+
expect(result.current.isPending).toBe(true);
116+
expect(result.current.isIdle).toBe(false);
117+
118+
await waitFor(() => {
119+
expect(result.current.isSuccess).toBe(true);
120+
});
121+
122+
expect(result.current.isPending).toBe(false);
123+
expect(result.current.isIdle).toBe(false);
124+
});
125+
126+
test("should handle multiple sequential mutations correctly", async () => {
127+
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);
128+
129+
const { result } = renderHook(
130+
() => useRevokeAccessTokenMutation(mockAuth),
131+
{
132+
wrapper,
133+
}
134+
);
135+
136+
act(() => {
137+
result.current.mutate(mockToken);
138+
});
139+
140+
await waitFor(() => {
141+
expect(result.current.isSuccess).toBe(true);
142+
});
143+
144+
// Reset mock
145+
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);
146+
147+
act(() => {
148+
result.current.mutate("different-token");
149+
});
150+
151+
await waitFor(() => {
152+
expect(result.current.isSuccess).toBe(true);
153+
});
154+
155+
expect(revokeAccessToken).toHaveBeenCalledTimes(2);
156+
expect(revokeAccessToken).toHaveBeenLastCalledWith(
157+
mockAuth,
158+
"different-token"
159+
);
160+
});
161+
162+
test("should reset mutation state correctly", async () => {
163+
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);
164+
165+
const { result } = renderHook(
166+
() => useRevokeAccessTokenMutation(mockAuth),
167+
{
168+
wrapper,
169+
}
170+
);
171+
172+
act(() => {
173+
result.current.mutate(mockToken);
174+
});
175+
176+
await waitFor(() => {
177+
expect(result.current.isSuccess).toBe(true);
178+
});
179+
180+
act(() => {
181+
result.current.reset();
182+
});
183+
184+
await waitFor(() => {
185+
expectInitialMutationState(result);
186+
});
187+
});
188+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import { type Auth, type AuthError, revokeAccessToken } from "firebase/auth";
3+
import { type AuthMutationOptions } from "./types";
4+
5+
export function useRevokeAccessTokenMutation(
6+
auth: Auth,
7+
options?: AuthMutationOptions<void, AuthError, string>
8+
) {
9+
return useMutation<void, AuthError, string>({
10+
...options,
11+
mutationFn: (token: string) => revokeAccessToken(auth, token),
12+
});
13+
}

packages/react/utils.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
1-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1+
import {
2+
QueryClient,
3+
QueryClientProvider,
4+
UseMutationResult,
5+
} from "@tanstack/react-query";
26
import React, { type ReactNode } from "react";
7+
import { expect } from "vitest";
38

49
const queryClient = new QueryClient({
5-
defaultOptions: {
6-
queries: {
7-
retry: false,
8-
},
9-
mutations: {
10-
retry: false,
11-
},
12-
},
10+
defaultOptions: {
11+
queries: {
12+
retry: false,
13+
},
14+
mutations: {
15+
retry: false,
16+
},
17+
},
1318
});
1419

1520
const wrapper = ({ children }: { children: ReactNode }) => (
16-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
21+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
1722
);
1823

1924
export { wrapper, queryClient };
2025

2126
// Helper type to make some properties of a type optional.
2227
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
28+
29+
export function expectInitialMutationState<TData, TError, TVariables>(result: {
30+
current: UseMutationResult<TData, TError, TVariables, unknown>;
31+
}) {
32+
expect(result.current.isSuccess).toBe(false);
33+
expect(result.current.isPending).toBe(false);
34+
expect(result.current.isError).toBe(false);
35+
expect(result.current.isIdle).toBe(true);
36+
expect(result.current.failureCount).toBe(0);
37+
expect(result.current.failureReason).toBeNull();
38+
expect(result.current.data).toBeUndefined();
39+
expect(result.current.error).toBeNull();
40+
}

0 commit comments

Comments
 (0)