import { renderHook, act, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const mockAuthFetch = vi.fn(); const mockAuth = { isAuthenticated: true, }; vi.mock('../../hooks/useAuthenticatedFetch', () => ({ useAuthenticatedFetch: () => ({ authFetch: mockAuthFetch, isAuthenticated: mockAuth.isAuthenticated, }), })); vi.mock('react-hot-toast', () => ({ default: { success: vi.fn(), error: vi.fn(), }, })); describe('useUserPreferences - subscription data', () => { let useUserPreferences: typeof import('../../hooks/useUserPreferences').useUserPreferences; beforeEach(async () => { vi.clearAllMocks(); vi.resetModules(); mockAuth.isAuthenticated = true; const module = await import('../../hooks/useUserPreferences'); useUserPreferences = module.useUserPreferences; }); afterEach(() => { vi.resetAllMocks(); }); describe('subscription info fetching', () => { it('fetches subscription status along with preferences', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'subscription', generationCount: 10, generationLimit: 250, generationsRemaining: 240, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription).toEqual({ hasAccess: true, accessType: 'subscription', generationCount: 10, generationLimit: 250, generationsRemaining: 240, }); }); it('sets subscription info for group access users', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'group', generationCount: null, generationLimit: null, generationsRemaining: null, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription).toEqual({ hasAccess: true, accessType: 'group', generationCount: null, generationLimit: null, generationsRemaining: null, }); }); it('sets subscription info for users without access', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: false, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: false, accessType: 'none', generationCount: 0, generationLimit: 250, generationsRemaining: 0, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription).toEqual({ hasAccess: false, accessType: 'none', generationCount: 0, generationLimit: 250, generationsRemaining: 0, }); }); it('handles subscription status fetch failure gracefully', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'purple', aiProvider: 'openrouter', hasAIAccess: false, }), }) .mockRejectedValueOnce(new Error('Network error')); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription).toBeNull(); expect(result.current.preferences.colorScheme).toBe('purple'); }); it('handles subscription status non-ok response gracefully', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: false, status: 503, }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription).toBeNull(); }); }); describe('hasAIAccess from preferences', () => { it('sets hasAIAccess true when user has AI access', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'group', }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.hasAIAccess).toBe(true); }); it('sets hasAIAccess false when user does not have AI access', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: false, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: false, accessType: 'none', }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.hasAIAccess).toBe(false); }); it('defaults hasAIAccess to false when not present', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: false, accessType: 'none', }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.hasAIAccess).toBe(false); }); }); describe('when not authenticated', () => { beforeEach(() => { mockAuth.isAuthenticated = false; }); it('does not fetch subscription status', async () => { const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(mockAuthFetch).not.toHaveBeenCalled(); expect(result.current.subscription).toBeNull(); }); }); describe('refetching subscription', () => { it('can refetch subscription info via fetchPreferences', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: false, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: false, accessType: 'none', generationCount: 0, generationLimit: 250, generationsRemaining: 0, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription?.hasAccess).toBe(false); mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'subscription', generationCount: 0, generationLimit: 250, generationsRemaining: 250, }), }); await act(async () => { await result.current.fetchPreferences(); }); expect(result.current.subscription?.hasAccess).toBe(true); expect(result.current.subscription?.accessType).toBe('subscription'); }); }); describe('generation tracking', () => { it('tracks generation count correctly', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'subscription', generationCount: 50, generationLimit: 250, generationsRemaining: 200, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription?.generationCount).toBe(50); expect(result.current.subscription?.generationsRemaining).toBe(200); }); it('shows zero remaining when limit reached', async () => { mockAuthFetch .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ colorScheme: 'blue', aiProvider: 'gemini', hasAIAccess: true, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: true, accessType: 'subscription', generationCount: 250, generationLimit: 250, generationsRemaining: 0, }), }); const { result } = renderHook(() => useUserPreferences()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.subscription?.generationsRemaining).toBe(0); }); }); });