From d8be81f4d1b1a88c734710a0449e98fb91bbc3d0 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 25 Sep 2024 20:42:26 +0300 Subject: [PATCH 1/3] tests(auth): add tests for useSignInAnonymouslyMutation --- .../useSignInAnonymouslyMutation.test.tsx | 134 ++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx index f059d85e..2de9d3a2 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx @@ -1,15 +1,29 @@ import React from "react"; -import { describe, expect, test, beforeEach, vi } from "vitest"; +import { + describe, + expect, + test, + beforeEach, + vi, + type MockInstance, +} from "vitest"; import { renderHook, act, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { type Auth, type UserCredential } from "firebase/auth"; +import { + type Auth, + type UserCredential, + signInAnonymously, +} from "firebase/auth"; import { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; +vi.mock("firebase/auth", () => ({ + signInAnonymously: vi.fn(), +})); + const queryClient = new QueryClient({ defaultOptions: { - queries: { - retry: false, - }, + queries: { retry: false }, + mutations: { retry: false }, }, }); @@ -17,13 +31,23 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); -describe("useSignInAnonymously", () => { +describe("useSignInAnonymouslyMutation", () => { let auth: Auth; + let mockSignInAnonymously: MockInstance; beforeEach(() => { + queryClient.clear(); + auth = {} as Auth; + mockSignInAnonymously = vi.mocked(signInAnonymously); + vi.clearAllMocks(); }); test("successfully signs in anonymously", async () => { + const mockUserCredential: UserCredential = { + user: { isAnonymous: true } as any, + } as UserCredential; + mockSignInAnonymously.mockResolvedValue(mockUserCredential); + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); @@ -37,9 +61,13 @@ describe("useSignInAnonymously", () => { }); expect(result.current.data?.user.isAnonymous).toBe(true); + expect(mockSignInAnonymously).toHaveBeenCalledWith(auth); }); - test("handles auth error", async () => { + test("handles sign-in error", async () => { + const mockError = new Error("Sign-in failed"); + mockSignInAnonymously.mockRejectedValue(mockError); + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); @@ -52,29 +80,113 @@ describe("useSignInAnonymously", () => { expect(result.current.isError).toBe(true); }); - // expect(result.current.error).toEqual(mockError); + expect(result.current.error).toEqual(mockError); }); - test("returns pending state initially", async () => { + test("goes through correct states when signing in anonymously", async () => { + const mockUserCredential: UserCredential = { + user: { isAnonymous: true } as any, + } as UserCredential; + + let resolveSignIn: (value: UserCredential) => void; + const signInPromise = new Promise((resolve) => { + resolveSignIn = resolve; + }); + + mockSignInAnonymously.mockReturnValue(signInPromise); + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); // Initially, it should be idle + expect(result.current.status).toBe("idle"); expect(result.current.isIdle).toBe(true); act(() => { result.current.mutate(); }); - // After mutate is called, it should be loading + // Immediately after calling mutate, it should be pending await waitFor(() => { + expect(result.current.status).toBe("pending"); expect(result.current.isPending).toBe(true); }); - // Once the request is resolved, it should be successful + // Resolve the sign-in promise + act(() => { + resolveSignIn(mockUserCredential); + }); + + // Finally, it should be success await waitFor(() => { + expect(result.current.status).toBe("success"); expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + + expect(mockSignInAnonymously).toHaveBeenCalledWith(auth); + }); + + test("can be called multiple times", async () => { + const mockUserCredential: UserCredential = { + user: { isAnonymous: true } as any, + } as UserCredential; + mockSignInAnonymously.mockResolvedValue(mockUserCredential); + + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + await act(async () => { + result.current.mutate(); }); + + await act(async () => { + result.current.mutate(); + }); + + await act(async () => { + result.current.mutate(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data?.user.isAnonymous).toBe(true); + expect(mockSignInAnonymously).toHaveBeenCalledTimes(3); + }); + + test("resets mutation state correctly", async () => { + const mockUserCredential: UserCredential = { + user: { isAnonymous: true }, + } as UserCredential; + mockSignInAnonymously.mockResolvedValue(mockUserCredential); + + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + act(() => { + result.current.mutateAsync(); + }); + + await waitFor(() => { + expect(result.current.data?.user.isAnonymous).toBe(true); + expect(result.current.isSuccess).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + + expect(mockSignInAnonymously).toHaveBeenCalledOnce(); }); }); From fc95e6e023580e8364e846e3d48030594e4bd058 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Thu, 26 Sep 2024 07:10:07 +0300 Subject: [PATCH 2/3] tests(auth); test useSignInAnonymouslyMutation with emulator --- package.json | 2 +- .../useSignInAnonymouslyMutation.test.tsx | 129 +----------------- vitest/utils.ts | 27 +++- 3 files changed, 34 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 66181155..4eaed52c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "vitest --dom --coverage", "serve:coverage": "npx serve coverage", - "emulator": "firebase emulators:start --project test-project --only firestore" + "emulators:start": "firebase emulators:start --project test-project" }, "devDependencies": { "@tanstack/react-query": "^5.55.4", diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx index 2de9d3a2..72dc2446 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx @@ -4,21 +4,14 @@ import { expect, test, beforeEach, + afterEach, vi, type MockInstance, } from "vitest"; import { renderHook, act, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { - type Auth, - type UserCredential, - signInAnonymously, -} from "firebase/auth"; import { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation"; - -vi.mock("firebase/auth", () => ({ - signInAnonymously: vi.fn(), -})); +import { auth, wipeAuth } from "~/testing-utils"; const queryClient = new QueryClient({ defaultOptions: { @@ -32,121 +25,21 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( ); describe("useSignInAnonymouslyMutation", () => { - let auth: Auth; - let mockSignInAnonymously: MockInstance; - - beforeEach(() => { + beforeEach(async () => { queryClient.clear(); - auth = {} as Auth; - mockSignInAnonymously = vi.mocked(signInAnonymously); - vi.clearAllMocks(); + await wipeAuth(); }); - test("successfully signs in anonymously", async () => { - const mockUserCredential: UserCredential = { - user: { isAnonymous: true } as any, - } as UserCredential; - mockSignInAnonymously.mockResolvedValue(mockUserCredential); - - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(); - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data?.user.isAnonymous).toBe(true); - expect(mockSignInAnonymously).toHaveBeenCalledWith(auth); + afterEach(async () => { + await auth.signOut(); }); - test("handles sign-in error", async () => { - const mockError = new Error("Sign-in failed"); - mockSignInAnonymously.mockRejectedValue(mockError); - - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(); - }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toEqual(mockError); - }); - - test("goes through correct states when signing in anonymously", async () => { - const mockUserCredential: UserCredential = { - user: { isAnonymous: true } as any, - } as UserCredential; - - let resolveSignIn: (value: UserCredential) => void; - const signInPromise = new Promise((resolve) => { - resolveSignIn = resolve; - }); - - mockSignInAnonymously.mockReturnValue(signInPromise); - + test("successfully signs in anonymously", async () => { const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); - // Initially, it should be idle - expect(result.current.status).toBe("idle"); - expect(result.current.isIdle).toBe(true); - - act(() => { - result.current.mutate(); - }); - - // Immediately after calling mutate, it should be pending - await waitFor(() => { - expect(result.current.status).toBe("pending"); - expect(result.current.isPending).toBe(true); - }); - - // Resolve the sign-in promise act(() => { - resolveSignIn(mockUserCredential); - }); - - // Finally, it should be success - await waitFor(() => { - expect(result.current.status).toBe("success"); - expect(result.current.isSuccess).toBe(true); - expect(result.current.data?.user.isAnonymous).toBe(true); - }); - - expect(mockSignInAnonymously).toHaveBeenCalledWith(auth); - }); - - test("can be called multiple times", async () => { - const mockUserCredential: UserCredential = { - user: { isAnonymous: true } as any, - } as UserCredential; - mockSignInAnonymously.mockResolvedValue(mockUserCredential); - - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { - wrapper, - }); - - await act(async () => { - result.current.mutate(); - }); - - await act(async () => { - result.current.mutate(); - }); - - await act(async () => { result.current.mutate(); }); @@ -155,15 +48,9 @@ describe("useSignInAnonymouslyMutation", () => { }); expect(result.current.data?.user.isAnonymous).toBe(true); - expect(mockSignInAnonymously).toHaveBeenCalledTimes(3); }); test("resets mutation state correctly", async () => { - const mockUserCredential: UserCredential = { - user: { isAnonymous: true }, - } as UserCredential; - mockSignInAnonymously.mockResolvedValue(mockUserCredential); - const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { wrapper, }); @@ -186,7 +73,5 @@ describe("useSignInAnonymouslyMutation", () => { expect(result.current.data).toBeUndefined(); expect(result.current.error).toBeNull(); }); - - expect(mockSignInAnonymously).toHaveBeenCalledOnce(); }); }); diff --git a/vitest/utils.ts b/vitest/utils.ts index f10031bc..d42d3996 100644 --- a/vitest/utils.ts +++ b/vitest/utils.ts @@ -1,4 +1,5 @@ import { type FirebaseApp, FirebaseError, initializeApp } from "firebase/app"; +import { getAuth, connectAuthEmulator, type Auth } from "firebase/auth"; import { getFirestore, connectFirestoreEmulator, @@ -8,16 +9,20 @@ import { expect } from "vitest"; const firebaseTestingOptions = { projectId: "test-project", + apiKey: "test-api-key", }; let app: FirebaseApp | undefined; let firestore: Firestore; +let auth: Auth; if (!app) { app = initializeApp(firebaseTestingOptions); firestore = getFirestore(app); + auth = getAuth(app); connectFirestoreEmulator(firestore, "localhost", 8080); + connectAuthEmulator(auth, "http://localhost:9099"); } async function wipeFirestore() { @@ -33,6 +38,19 @@ async function wipeFirestore() { } } +async function wipeAuth() { + const response = await fetch( + "http://localhost:9099/emulator/v1/projects/test-project/accounts", + { + method: "DELETE", + } + ); + + if (!response.ok) { + throw new Error("Failed to wipe auth"); + } +} + function expectFirestoreError(error: unknown, expectedCode: string) { if (error instanceof FirebaseError) { expect(error).toBeDefined(); @@ -45,4 +63,11 @@ function expectFirestoreError(error: unknown, expectedCode: string) { } } -export { firestore, wipeFirestore, expectFirestoreError }; +export { + firestore, + wipeFirestore, + expectFirestoreError, + firebaseTestingOptions, + auth, + wipeAuth, +}; From 0a801984cf88b1c25f85c783e0900021429a5872 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Thu, 26 Sep 2024 07:55:24 +0300 Subject: [PATCH 3/3] test(auth): test useSignInAnonymouslyMutation for multiple sequential sign-ins --- .../useSignInAnonymouslyMutation.test.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx index 72dc2446..f52b97f2 100644 --- a/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx +++ b/packages/react/src/auth/useSignInAnonymouslyMutation.test.tsx @@ -74,4 +74,41 @@ describe("useSignInAnonymouslyMutation", () => { expect(result.current.error).toBeNull(); }); }); + + test("allows multiple sequential sign-ins", async () => { + const { result } = renderHook(() => useSignInAnonymouslyMutation(auth), { + wrapper, + }); + + // First sign-in + act(() => { + result.current.mutate(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + + // Reset state + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.isIdle).toBe(true); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeNull(); + }); + + // Second sign-in + act(() => { + result.current.mutate(); + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.user.isAnonymous).toBe(true); + }); + }); });