Skip to content

Commit

Permalink
feat(react): add useRevokeAccessTokenMutation
Browse files Browse the repository at this point in the history
  • Loading branch information
HassanBahati committed Jan 15, 2025
1 parent 5e6e46a commit 3c9b2ad
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// useCreateUserWithEmailAndPasswordMutation
// useFetchSignInMethodsForEmailQuery
// useGetRedirectResultQuery
// useRevokeAccessTokenMutation
export { useRevokeAccessTokenMutation } from "./useRevokeAccessTokenMutation";
// useSendPasswordResetEmailMutation
export { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMutation";
export { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation";
Expand Down
7 changes: 7 additions & 0 deletions packages/react/src/auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UseMutationOptions } from "@tanstack/react-query";

export type AuthMutationOptions<
TData = unknown,
TError = Error,
TVariables = void
> = Omit<UseMutationOptions<TData, TError, TVariables>, "mutationFn">;
188 changes: 188 additions & 0 deletions packages/react/src/auth/useRevokeAccessTokenMutation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { useRevokeAccessTokenMutation } from "./useRevokeAccessTokenMutation";
import { Auth, revokeAccessToken } from "firebase/auth";
import { describe, expect, test, vi, beforeEach } from "vitest";
import { act, renderHook, waitFor } from "@testing-library/react";
import { wrapper, queryClient, expectInitialMutationState } from "../../utils";

vi.mock("firebase/auth", () => ({
...vi.importActual("firebase/auth"),
revokeAccessToken: vi.fn(),
}));

describe("useRevokeAccessTokenMutation", () => {
const mockAuth = {} as Auth;
const mockToken = "mock-access-token";

beforeEach(() => {
vi.clearAllMocks();
queryClient.clear();
});

test("should successfully revoke access token", async () => {
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);

const { result } = renderHook(
() => useRevokeAccessTokenMutation(mockAuth),
{
wrapper,
}
);

act(() => {
result.current.mutate(mockToken);
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(revokeAccessToken).toHaveBeenCalledTimes(1);
expect(revokeAccessToken).toHaveBeenCalledWith(mockAuth, mockToken);
});

test("should handle revocation failure", async () => {
const mockError = new Error("Failed to revoke token");
vi.mocked(revokeAccessToken).mockRejectedValueOnce(mockError);

const { result } = renderHook(
() => useRevokeAccessTokenMutation(mockAuth),
{
wrapper,
}
);

act(() => {
result.current.mutate(mockToken);
});

await waitFor(() => {
expect(result.current.isError).toBe(true);
});

expect(result.current.error).toBe(mockError);
expect(revokeAccessToken).toHaveBeenCalledTimes(1);
expect(revokeAccessToken).toHaveBeenCalledWith(mockAuth, mockToken);
});

test("should accept and use custom mutation options", async () => {
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);

const onSuccessMock = vi.fn();
const onErrorMock = vi.fn();

const { result } = renderHook(
() =>
useRevokeAccessTokenMutation(mockAuth, {
onSuccess: onSuccessMock,
onError: onErrorMock,
}),
{
wrapper,
}
);

act(() => {
result.current.mutate(mockToken);
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(onSuccessMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).not.toHaveBeenCalled();
});

test("should properly handle loading state throughout mutation lifecycle", async () => {
vi.mocked(revokeAccessToken).mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 100))
);

const { result } = renderHook(
() => useRevokeAccessTokenMutation(mockAuth),
{
wrapper,
}
);

expect(result.current.isPending).toBe(false);
expect(result.current.isIdle).toBe(true);

await act(async () => {
await result.current.mutateAsync(mockToken);
});

expect(result.current.isPending).toBe(true);
expect(result.current.isIdle).toBe(false);

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.isPending).toBe(false);
expect(result.current.isIdle).toBe(false);
});

test("should handle multiple sequential mutations correctly", async () => {
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);

const { result } = renderHook(
() => useRevokeAccessTokenMutation(mockAuth),
{
wrapper,
}
);

act(() => {
result.current.mutate(mockToken);
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

// Reset mock
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);

act(() => {
result.current.mutate("different-token");
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(revokeAccessToken).toHaveBeenCalledTimes(2);
expect(revokeAccessToken).toHaveBeenLastCalledWith(
mockAuth,
"different-token"
);
});

test("should reset mutation state correctly", async () => {
vi.mocked(revokeAccessToken).mockResolvedValueOnce(undefined);

const { result } = renderHook(
() => useRevokeAccessTokenMutation(mockAuth),
{
wrapper,
}
);

act(() => {
result.current.mutate(mockToken);
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

act(() => {
result.current.reset();
});

await waitFor(() => {
expectInitialMutationState(result);
});
});
});
13 changes: 13 additions & 0 deletions packages/react/src/auth/useRevokeAccessTokenMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useMutation } from "@tanstack/react-query";
import { type Auth, type AuthError, revokeAccessToken } from "firebase/auth";
import { type AuthMutationOptions } from "./types";

export function useRevokeAccessTokenMutation(
auth: Auth,
options?: AuthMutationOptions<void, AuthError, string>
) {
return useMutation<void, AuthError, string>({
...options,
mutationFn: (token: string) => revokeAccessToken(auth, token),
});
}
38 changes: 28 additions & 10 deletions packages/react/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
QueryClient,
QueryClientProvider,
UseMutationResult,
} from "@tanstack/react-query";
import React, { type ReactNode } from "react";
import { expect } from "vitest";

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});

const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

export { wrapper, queryClient };

// Helper type to make some properties of a type optional.
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

export function expectInitialMutationState<TData, TError, TVariables>(result: {
current: UseMutationResult<TData, TError, TVariables, unknown>;
}) {
expect(result.current.isSuccess).toBe(false);
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isIdle).toBe(true);
expect(result.current.failureCount).toBe(0);
expect(result.current.failureReason).toBeNull();
expect(result.current.data).toBeUndefined();
expect(result.current.error).toBeNull();
}

0 comments on commit 3c9b2ad

Please sign in to comment.