From 2db7e1f9f535b78ffc6275c6e85a50225475aaa3 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Sat, 18 Apr 2026 18:49:54 +0530 Subject: [PATCH] test: enhance tests to be specific --- frontend/src/api/apiClient.test.ts | 15 +--- frontend/src/hooks/useAuth.test.ts | 8 -- frontend/src/hooks/useLetters.test.ts | 113 ++++++++++++++++++++++++++ frontend/src/pages/Drawer.test.tsx | 53 +++++++++++- frontend/src/utils/crypto.test.ts | 2 +- 5 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 frontend/src/hooks/useLetters.test.ts diff --git a/frontend/src/api/apiClient.test.ts b/frontend/src/api/apiClient.test.ts index 074bc8e..a9898ec 100644 --- a/frontend/src/api/apiClient.test.ts +++ b/frontend/src/api/apiClient.test.ts @@ -1,13 +1,5 @@ import { HttpResponse, http } from "msw"; -import { - afterAll, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { mockUser } from "../../test/fixtures/user.fixture"; import { server } from "../../test/mocks/server"; import { useAuthStore } from "../store/useAuthStore"; @@ -21,13 +13,10 @@ beforeEach(() => { user: null, isInitializing: false, }); -}); - -beforeAll(() => { vi.stubEnv("VITE_API_URL", VITE_API_URL); }); -afterAll(() => { +afterEach(() => { vi.unstubAllEnvs(); }); diff --git a/frontend/src/hooks/useAuth.test.ts b/frontend/src/hooks/useAuth.test.ts index b397d3b..c6d0f43 100644 --- a/frontend/src/hooks/useAuth.test.ts +++ b/frontend/src/hooks/useAuth.test.ts @@ -6,7 +6,6 @@ import { mockUser } from "../../test/fixtures/user.fixture"; import { server } from "../../test/mocks/server"; import { useAuthStore } from "../store/useAuthStore"; import { useKeyStore } from "../store/useKeyStore"; -import { CryptoUtils } from "../utils/crypto"; import { clearMasterKey, loadMasterKey, @@ -14,7 +13,6 @@ import { } from "../utils/keystore"; import { useAuth } from "./useAuth"; -vi.mock("../utils/crypto"); vi.mock("../utils/keystore"); const VITE_API_URL = "http://piku-server"; @@ -22,12 +20,6 @@ const VITE_API_URL = "http://piku-server"; beforeEach(() => { vi.clearAllMocks(); - // hack to set up mock implementations using fixtures - vi.mocked(CryptoUtils.deriveKeyBundle).mockResolvedValue({ - masterKey: mockMasterKey, - authHash: "mock-auth-hash", - }); - vi.mocked(loadMasterKey).mockResolvedValue(mockMasterKey); vi.mocked(saveMasterKey).mockResolvedValue("masterKey"); vi.mocked(clearMasterKey).mockResolvedValue(undefined); diff --git a/frontend/src/hooks/useLetters.test.ts b/frontend/src/hooks/useLetters.test.ts new file mode 100644 index 0000000..83a3d31 --- /dev/null +++ b/frontend/src/hooks/useLetters.test.ts @@ -0,0 +1,113 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { HttpResponse, http } from "msw"; +import { beforeEach, describe, expect, it } from "vitest"; +import { server } from "../../test/mocks/server"; +import { endpoints } from "../config/endpoints"; +import { useKeyStore } from "../store/useKeyStore"; +import { CryptoUtils } from "../utils/crypto"; +import { useLetters } from "./useLetters"; + +describe("useLetters hook", () => { + let masterKey: CryptoKey; + let utils: CryptoUtils; + + beforeEach(async () => { + utils = new CryptoUtils(); + await utils.initialize(); + const bundle = await CryptoUtils.deriveKeyBundle("password", "salt"); + masterKey = bundle.masterKey; + + useKeyStore.setState({ masterKey: null }); + }); + + it("should indicate authentication is required when masterKey is missing", () => { + const { result } = renderHook(() => useLetters()); + + expect(result.current.isAuthRequired).toBe(true); + }); + + it("should fetch, decrypt, and categorize letters when masterKey is present", async () => { + useKeyStore.setState({ masterKey }); + + const draftPayload = { objects: [] }; + const encryptedDraft = await utils.encryptMetadata( + { recipient: "Draft Recipient" }, + masterKey, + ); + + const lettersResponse = [ + { + public_id: "letter-1", + type: "KEPT", + status: "DRAFT", + updated_at: new Date().toISOString(), + encrypted_metadata: encryptedDraft.encrypted_content, + encrypted_content: JSON.stringify(draftPayload), + encrypted_dek: encryptedDraft.encrypted_dek, + }, + ]; + + server.use( + http.get(`${import.meta.env.VITE_API_URL}${endpoints.LETTERS}`, () => { + return HttpResponse.json(lettersResponse); + }), + ); + + const { result } = renderHook(() => useLetters()); + + // Initially loading + expect(result.current.loading).toBe(true); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.drafts).toHaveLength(1); + expect(result.current.drafts[0].metadata.recipient).toBe("Draft Recipient"); + expect(result.current.kept).toHaveLength(0); + }); + + it("should sort letters by updated_at in descending order", async () => { + useKeyStore.setState({ masterKey }); + + const metadata = await utils.encryptMetadata( + { recipient: "test" }, + masterKey, + ); + + const now = new Date(); + const older = new Date(now.getTime() - 10000); + + const lettersResponse = [ + { + public_id: "older", + type: "KEPT", + status: "SEALED", + updated_at: older.toISOString(), + encrypted_metadata: metadata.encrypted_content, + encrypted_content: "{}", + encrypted_dek: metadata.encrypted_dek, + }, + { + public_id: "newer", + type: "KEPT", + status: "SEALED", + updated_at: now.toISOString(), + encrypted_metadata: metadata.encrypted_content, + encrypted_content: "{}", + encrypted_dek: metadata.encrypted_dek, + }, + ]; + + server.use( + http.get(`${import.meta.env.VITE_API_URL}${endpoints.LETTERS}`, () => { + return HttpResponse.json(lettersResponse); + }), + ); + + const { result } = renderHook(() => useLetters()); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.kept[0].public_id).toBe("newer"); + expect(result.current.kept[1].public_id).toBe("older"); + }); +}); diff --git a/frontend/src/pages/Drawer.test.tsx b/frontend/src/pages/Drawer.test.tsx index 414c9e3..e8657c6 100644 --- a/frontend/src/pages/Drawer.test.tsx +++ b/frontend/src/pages/Drawer.test.tsx @@ -1,10 +1,13 @@ import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { mockUser } from "../../test/fixtures/user.fixture"; +import { useLetters } from "../hooks/useLetters"; import { useAuthStore } from "../store/useAuthStore"; import Drawer from "./Drawer"; +vi.mock("../hooks/useLetters"); + describe("Drawer Page", () => { beforeEach(() => { // Setup authenticated state for the test @@ -13,6 +16,15 @@ describe("Drawer Page", () => { accessToken: "fake-token", isInitializing: false, }); + + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: false, + }); }); it("renders the cabinet sections and empty state message", () => { @@ -27,4 +39,43 @@ describe("Drawer Page", () => { expect(screen.getByText(/Vault/i)).toBeInTheDocument(); expect(screen.getByText(/This drawer remains silent/i)).toBeInTheDocument(); }); + + it("renders the loading state", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: true, + isAuthRequired: false, + }); + + render( + + + , + ); + + expect(screen.getByText(/Opening your cabinet/i)).toBeInTheDocument(); + }); + + it("renders the authentication required modal when api requires auth", () => { + vi.mocked(useLetters).mockReturnValue({ + drafts: [], + kept: [], + sent: [], + vault: [], + loading: false, + isAuthRequired: true, + }); + + render( + + + , + ); + + expect(screen.getByText(/Authentication Required/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument(); + }); }); diff --git a/frontend/src/utils/crypto.test.ts b/frontend/src/utils/crypto.test.ts index bcd282f..549d8bd 100644 --- a/frontend/src/utils/crypto.test.ts +++ b/frontend/src/utils/crypto.test.ts @@ -169,7 +169,7 @@ describe("encryptImage / decryptImage", () => { }); }); -describe("Sharing Key Decryption (TDD)", () => { +describe("Sharing Key Decryption", () => { let masterKey: CryptoKey; beforeEach(async () => { const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");