diff --git a/src/hooks/__tests__/useAuth.test.ts b/src/hooks/__tests__/useAuth.test.ts new file mode 100644 index 00000000..6b947d53 --- /dev/null +++ b/src/hooks/__tests__/useAuth.test.ts @@ -0,0 +1,81 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { useWalletStore } from "@/store/walletStore"; +import { useAuth } from "../useAuth"; + +jest.mock("@/store/walletStore", () => ({ + useWalletStore: jest.fn(), +})); + +const mockUseWalletStore = useWalletStore as jest.MockedFunction< + typeof useWalletStore +>; + +const connectedAddress = "0x1234567890123456789012345678901234567890"; + +const setWalletState = ( + overrides: Partial> = {}, +) => { + mockUseWalletStore.mockReturnValue({ + address: null, + isConnected: false, + ...overrides, + } as ReturnType); +}; + +describe("useAuth", () => { + beforeEach(() => { + jest.clearAllMocks(); + document.cookie = ""; + setWalletState(); + }); + + it("reports an unauthenticated state with no connected wallet and no cookie", async () => { + const { result } = renderHook(() => useAuth()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.userAddress).toBeNull(); + }); + + it("does not authenticate from cookie presence alone", async () => { + document.cookie = "auth-token=garbage-token"; + const { result } = renderHook(() => useAuth()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.userAddress).toBeNull(); + }); + + it("does not authenticate from an expired or invalid cookie payload", async () => { + document.cookie = "auth-token=eyJhbGciOiJIUzI1NiJ9.invalid-payload"; + const { result } = renderHook(() => useAuth()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.userAddress).toBeNull(); + }); + + it("authenticates with the connected wallet address", async () => { + setWalletState({ address: connectedAddress, isConnected: true }); + const { result } = renderHook(() => useAuth()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isAuthenticated).toBe(true); + expect(result.current.userAddress).toBe(connectedAddress); + expect(result.current.userAddress).not.toBe("0x..."); + }); + + it("does not authenticate when the wallet has an address but is disconnected", async () => { + setWalletState({ address: connectedAddress, isConnected: false }); + const { result } = renderHook(() => useAuth()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.userAddress).toBeNull(); + }); +}); diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 39f9f8ee..0b5cadb0 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,9 +1,9 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from "react"; +import { useWalletStore } from "@/store/walletStore"; -const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minutes -const WARN_BEFORE_MS = 5 * 60 * 1000; // warn 5 minutes before expiry +const WARN_BEFORE_MS = 5 * 60 * 1000; interface AuthState { isAuthenticated: boolean; @@ -13,56 +13,20 @@ interface AuthState { } export function useAuth() { - const [authState, setAuthState] = useState({ - isAuthenticated: false, - isLoading: true, - userAddress: null, - sessionExpiresAt: null, - }); + const { address, isConnected } = useWalletStore(); + const [isHydrated, setIsHydrated] = useState(false); useEffect(() => { - const checkAuth = async () => { - try { - const tokenMatch = document.cookie.match(/auth-token=([^;]+)/); - const token = tokenMatch ? tokenMatch[1] : null; - let isValid = false; - let expiresAt = null; - let address = null; - - if (token) { - try { - const payloadBase64 = token.split('.')[1]; - if (payloadBase64) { - const payload = JSON.parse(atob(payloadBase64)); - if (payload.exp && payload.exp * 1000 > Date.now()) { - isValid = true; - expiresAt = payload.exp * 1000; - address = payload.address || '0x...'; - } - } - } catch (e) { - // Invalid token format - } - } - - setAuthState({ - isAuthenticated: isValid, - isLoading: false, - userAddress: address, - sessionExpiresAt: expiresAt, - }); - } catch { - setAuthState({ - isAuthenticated: false, - isLoading: false, - userAddress: null, - sessionExpiresAt: null, - }); - } - }; - - checkAuth(); + setIsHydrated(true); }, []); - return { ...authState, WARN_BEFORE_MS }; + const hasVerifiedWallet = isHydrated && isConnected && Boolean(address); + + return { + isAuthenticated: hasVerifiedWallet, + isLoading: !isHydrated, + userAddress: hasVerifiedWallet ? address : null, + sessionExpiresAt: null, + WARN_BEFORE_MS, + } satisfies AuthState & { WARN_BEFORE_MS: number }; } diff --git a/useAuth.ts b/useAuth.ts index b174950f..426a40ca 100644 --- a/useAuth.ts +++ b/useAuth.ts @@ -1,47 +1 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { useWalletConnector } from './useWalletConnector'; - -interface AuthState { - isAuthenticated: boolean; - isLoading: boolean; - userAddress: string | null; -} - -export function useAuth() { - const { connectWallet } = useWalletConnector(); - const [authState, setAuthState] = useState({ - isAuthenticated: false, - isLoading: true, - userAddress: null, - }); - - useEffect(() => { - // Check for existing session/token on mount - const checkAuth = async () => { - try { - // In a real Web3 app, we'd check if the wallet is still connected - // and if a valid session token exists in cookies - const hasToken = document.cookie.includes('auth-token='); - - // Mocking check - in production, validate JWT or wallet state here - setAuthState({ - isAuthenticated: hasToken, - isLoading: false, - userAddress: hasToken ? '0x...' : null, // Get from wallet provider - }); - } catch (error) { - setAuthState({ - isAuthenticated: false, - isLoading: false, - userAddress: null, - }); - } - }; - - checkAuth(); - }, []); - - return authState; -} \ No newline at end of file +export { useAuth } from "./src/hooks/useAuth";