import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mockNuxtImport } from '@nuxt/test-utils/runtime'; import type { AuthProviderInfo, AuthModel, RecordModel } from 'pocketbase'; /** * Comprehensive tests for useAuth composable * Based on specs from private/specs.md section 3.1 (Authentication API) * * These tests verify actual behavior, not just API existence. */ // Mock PocketBase const mockAuthStore = { isValid: false, record: null as AuthModel | null, clear: vi.fn(), onChange: vi.fn(), }; const mockCollection = vi.fn(); const mockAuthWithOAuth2 = vi.fn(); const mockListAuthMethods = vi.fn(); const mockAuthRefresh = vi.fn(); vi.mock('../usePocketbase', () => ({ usePocketbase: () => ({ authStore: mockAuthStore, collection: mockCollection, }), })); // Mock router using Nuxt's test utils const mockRouterPush = vi.fn(); const mockRouter = { push: mockRouterPush, }; mockNuxtImport('useRouter', () => { return () => mockRouter; }); describe('useAuth', () => { beforeEach(async () => { // Reset all mocks vi.clearAllMocks(); mockAuthStore.isValid = false; mockAuthStore.record = null; // Setup default mock implementations mockCollection.mockReturnValue({ authWithOAuth2: mockAuthWithOAuth2, listAuthMethods: mockListAuthMethods, authRefresh: mockAuthRefresh, }); // Clear module cache to get fresh imports vi.resetModules(); }); describe('Composable Export', () => { it('should be exported as a function', async () => { const { useAuth } = await import('../useAuth'); expect(useAuth).toBeDefined(); expect(typeof useAuth).toBe('function'); }); it('should return an object with auth methods and state', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); expect(auth).toBeDefined(); expect(typeof auth).toBe('object'); }); }); describe('Initial State', () => { it('should initialize with user as null', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); expect(auth.user.value).toBeNull(); }); it('should initialize with isAuthenticated as false', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); expect(auth.isAuthenticated.value).toBe(false); }); it('should initialize with loading as false', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); expect(auth.loading.value).toBe(false); }); it('should initialize with error as null', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); expect(auth.error.value).toBeNull(); }); }); describe('initAuth', () => { it('should sync user from authStore', async () => { const mockUser: AuthModel = { id: 'user123', email: 'test@example.com', } as unknown as AuthModel; mockAuthStore.record = mockUser; mockAuthStore.isValid = true; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); expect(auth.user.value).toEqual(mockUser); }); it('should register onChange listener', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); expect(mockAuthStore.onChange).toHaveBeenCalled(); }); it('should update user when authStore changes', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); // Get the onChange callback const onChangeCallback = mockAuthStore.onChange.mock.calls[0]?.[0]; // Simulate auth change const newUser = { id: 'newUser456', email: 'new@example.com', }; onChangeCallback('token123', newUser); expect(auth.user.value).toEqual(newUser); }); }); describe('login', () => { const mockProviders: AuthProviderInfo[] = [ { name: 'google', displayName: 'Google', state: 'state123', codeVerifier: 'verifier', codeChallenge: 'challenge', codeChallengeMethod: 'S256', authURL: 'https://google.com/oauth', }, { name: 'microsoft', displayName: 'Microsoft', state: 'state456', codeVerifier: 'verifier2', codeChallenge: 'challenge2', codeChallengeMethod: 'S256', authURL: 'https://microsoft.com/oauth', }, ]; beforeEach(() => { mockListAuthMethods.mockResolvedValue({ oauth2: { enabled: true, providers: mockProviders, }, }); }); it('should set loading to true when login starts', async () => { mockAuthWithOAuth2.mockImplementation(() => new Promise(() => {})); // Never resolves const { useAuth } = await import('../useAuth'); const auth = useAuth(); const loginPromise = auth.login('google'); expect(auth.loading.value).toBe(true); // Cleanup await Promise.race([loginPromise, new Promise((resolve) => setTimeout(resolve, 10))]); }); it('should clear previous errors when starting new login', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); // Set an error first auth.error.value = new Error('Previous error'); mockAuthWithOAuth2.mockResolvedValue({}); await auth.login('google'); expect(auth.error.value).toBeNull(); }); it('should call authWithOAuth2 with correct provider', async () => { mockAuthWithOAuth2.mockResolvedValue({}); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('google'); expect(mockAuthWithOAuth2).toHaveBeenCalledWith({ provider: 'google' }); }); it('should set loading to false after login completes', async () => { mockAuthWithOAuth2.mockResolvedValue({}); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('google'); expect(auth.loading.value).toBe(false); }); it('should throw error if provider is not configured', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('github'); // Not in mockProviders expect(auth.error.value).toBeDefined(); expect(auth.error.value?.message).toContain('github'); expect(auth.error.value?.message).toContain('not configured'); }); it('should handle OAuth errors gracefully', async () => { const mockError = new Error('OAuth failed'); mockAuthWithOAuth2.mockRejectedValue(mockError); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('google'); expect(auth.error.value).toEqual(mockError); expect(auth.loading.value).toBe(false); }); it('should support multiple OAuth providers', async () => { mockAuthWithOAuth2.mockResolvedValue({}); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('google'); expect(mockAuthWithOAuth2).toHaveBeenCalledWith({ provider: 'google' }); await auth.login('microsoft'); expect(mockAuthWithOAuth2).toHaveBeenCalledWith({ provider: 'microsoft' }); }); it('should return empty array when OAuth is disabled', async () => { mockListAuthMethods.mockResolvedValue({ oauth2: { enabled: false, providers: [], }, }); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.login('google'); expect(auth.error.value).toBeDefined(); expect(auth.error.value?.message).toContain('not configured'); }); }); describe('handleOAuthCallback', () => { it('should sync user from authStore', async () => { const mockUser: AuthModel = { id: 'user123', email: 'test@example.com', } as unknown as AuthModel; mockAuthStore.record = mockUser; mockAuthStore.isValid = true; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.handleOAuthCallback(); expect(auth.user.value).toEqual(mockUser); }); it('should redirect to dashboard when authenticated', async () => { mockAuthStore.record = { id: 'user123', email: 'test@example.com', } as unknown as RecordModel; mockAuthStore.isValid = true; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.handleOAuthCallback(); expect(mockRouterPush).toHaveBeenCalledWith('/dashboard'); }); it('should redirect to home when not authenticated', async () => { mockAuthStore.record = null; mockAuthStore.isValid = false; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.handleOAuthCallback(); expect(mockRouterPush).toHaveBeenCalledWith('/'); }); }); describe('refreshAuth', () => { it('should call authRefresh on users collection', async () => { mockAuthRefresh.mockResolvedValue({ token: 'newToken', record: { id: 'user123', email: 'test@example.com' }, }); const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.refreshAuth(); expect(mockCollection).toHaveBeenCalledWith('users'); expect(mockAuthRefresh).toHaveBeenCalled(); }); it('should return the refresh result', async () => { const mockResult = { token: 'newToken', record: { id: 'user123', email: 'test@example.com' }, }; mockAuthRefresh.mockResolvedValue(mockResult); const { useAuth } = await import('../useAuth'); const auth = useAuth(); const result = await auth.refreshAuth(); expect(result).toEqual(mockResult); }); }); describe('logout', () => { it('should clear authStore', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); auth.logout(); expect(mockAuthStore.clear).toHaveBeenCalled(); }); it('should clear authStore even when user is authenticated', async () => { mockAuthStore.record = { id: 'user123', email: 'test@example.com', } as unknown as RecordModel; mockAuthStore.isValid = true; const { useAuth } = await import('../useAuth'); const auth = useAuth(); auth.logout(); expect(mockAuthStore.clear).toHaveBeenCalled(); }); }); describe('isAuthenticated computed', () => { it('should be false when authStore is invalid', async () => { mockAuthStore.isValid = false; mockAuthStore.record = { id: 'user123', email: 'test@example.com' } as unknown as RecordModel; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); expect(auth.isAuthenticated.value).toBe(false); }); it('should be false when user is null', async () => { mockAuthStore.isValid = true; mockAuthStore.record = null; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); expect(auth.isAuthenticated.value).toBe(false); }); it('should be true when authStore is valid and user exists', async () => { mockAuthStore.isValid = true; mockAuthStore.record = { id: 'user123', email: 'test@example.com' } as unknown as RecordModel; const { useAuth } = await import('../useAuth'); const auth = useAuth(); await auth.initAuth(); expect(auth.isAuthenticated.value).toBe(true); }); }); describe('Exposed API', () => { it('should expose all required methods and properties', async () => { const { useAuth } = await import('../useAuth'); const auth = useAuth(); // State expect(auth.user).toBeDefined(); expect(auth.isAuthenticated).toBeDefined(); expect(auth.loading).toBeDefined(); expect(auth.error).toBeDefined(); // Methods expect(typeof auth.initAuth).toBe('function'); expect(typeof auth.login).toBe('function'); expect(typeof auth.logout).toBe('function'); expect(typeof auth.handleOAuthCallback).toBe('function'); expect(typeof auth.refreshAuth).toBe('function'); }); }); });