import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; // Mock dependencies vi.mock('../../api/oauth', () => ({ getOAuthProviders: vi.fn(), getOAuthConnections: vi.fn(), initiateOAuth: vi.fn(), handleOAuthCallback: vi.fn(), disconnectOAuth: vi.fn(), })); vi.mock('../../utils/cookies', () => ({ setCookie: vi.fn(), })); import { useOAuthProviders, useOAuthConnections, useInitiateOAuth, useOAuthCallback, useDisconnectOAuth, } from '../useOAuth'; import * as oauthApi from '../../api/oauth'; import * as cookies from '../../utils/cookies'; // Create a wrapper with QueryClientProvider const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); return function Wrapper({ children }: { children: React.ReactNode }) { return React.createElement( QueryClientProvider, { client: queryClient }, children ); }; }; describe('useOAuth hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useOAuthProviders', () => { it('fetches OAuth providers successfully', async () => { const mockProviders: oauthApi.OAuthProvider[] = [ { name: 'google', display_name: 'Google', icon: 'https://example.com/google.png', }, { name: 'microsoft', display_name: 'Microsoft', icon: 'https://example.com/microsoft.png', }, ]; vi.mocked(oauthApi.getOAuthProviders).mockResolvedValue(mockProviders); const { result } = renderHook(() => useOAuthProviders(), { wrapper: createWrapper(), }); expect(result.current.isLoading).toBe(true); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data).toEqual(mockProviders); expect(oauthApi.getOAuthProviders).toHaveBeenCalledTimes(1); }); it('handles errors when fetching providers fails', async () => { const mockError = new Error('Failed to fetch providers'); vi.mocked(oauthApi.getOAuthProviders).mockRejectedValue(mockError); const { result } = renderHook(() => useOAuthProviders(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.isError).toBe(true); expect(result.current.error).toEqual(mockError); }); it('uses correct query configuration', () => { vi.mocked(oauthApi.getOAuthProviders).mockResolvedValue([]); const { result } = renderHook(() => useOAuthProviders(), { wrapper: createWrapper(), }); // The hook should be configured with staleTime and refetchOnWindowFocus // We can verify this by checking that the hook doesn't refetch immediately expect(result.current.isLoading).toBe(true); }); }); describe('useOAuthConnections', () => { it('fetches OAuth connections successfully', async () => { const mockConnections: oauthApi.OAuthConnection[] = [ { id: '1', provider: 'google', provider_user_id: 'user123', email: 'test@example.com', connected_at: '2025-01-01T00:00:00Z', }, { id: '2', provider: 'microsoft', provider_user_id: 'user456', email: 'test@microsoft.com', connected_at: '2025-01-02T00:00:00Z', }, ]; vi.mocked(oauthApi.getOAuthConnections).mockResolvedValue(mockConnections); const { result } = renderHook(() => useOAuthConnections(), { wrapper: createWrapper(), }); expect(result.current.isLoading).toBe(true); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data).toEqual(mockConnections); expect(oauthApi.getOAuthConnections).toHaveBeenCalledTimes(1); }); it('handles errors when fetching connections fails', async () => { const mockError = new Error('Failed to fetch connections'); vi.mocked(oauthApi.getOAuthConnections).mockRejectedValue(mockError); const { result } = renderHook(() => useOAuthConnections(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.isError).toBe(true); expect(result.current.error).toEqual(mockError); }); it('returns empty array when no connections exist', async () => { vi.mocked(oauthApi.getOAuthConnections).mockResolvedValue([]); const { result } = renderHook(() => useOAuthConnections(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data).toEqual([]); }); }); describe('useInitiateOAuth', () => { it('initiates OAuth flow and redirects to authorization URL', async () => { const mockAuthUrl = 'https://accounts.google.com/oauth/authorize?client_id=123'; vi.mocked(oauthApi.initiateOAuth).mockResolvedValue({ authorization_url: mockAuthUrl, }); // Mock window.location const originalLocation = window.location; delete (window as any).location; window.location = { ...originalLocation, href: '' } as Location; const { result } = renderHook(() => useInitiateOAuth(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync('google'); }); expect(oauthApi.initiateOAuth).toHaveBeenCalledWith('google'); expect(window.location.href).toBe(mockAuthUrl); // Restore window.location window.location = originalLocation; }); it('handles errors when initiating OAuth fails', async () => { const mockError = new Error('Failed to initiate OAuth'); vi.mocked(oauthApi.initiateOAuth).mockRejectedValue(mockError); const { result } = renderHook(() => useInitiateOAuth(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync('google'); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); it('supports multiple OAuth providers', async () => { const providers = ['google', 'microsoft', 'github']; for (const provider of providers) { vi.mocked(oauthApi.initiateOAuth).mockResolvedValue({ authorization_url: `https://${provider}.com/oauth/authorize`, }); const originalLocation = window.location; delete (window as any).location; window.location = { ...originalLocation, href: '' } as Location; const { result } = renderHook(() => useInitiateOAuth(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(provider); }); expect(oauthApi.initiateOAuth).toHaveBeenCalledWith(provider); expect(window.location.href).toBe(`https://${provider}.com/oauth/authorize`); window.location = originalLocation; vi.clearAllMocks(); } }); }); describe('useOAuthCallback', () => { it('handles OAuth callback and stores tokens in cookies', async () => { const mockResponse: oauthApi.OAuthTokenResponse = { access: 'access-token-123', refresh: 'refresh-token-456', user: { id: 1, username: 'testuser', email: 'test@example.com', name: 'Test User', role: 'owner', is_staff: false, is_superuser: false, }, }; vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse); const { result } = renderHook(() => useOAuthCallback(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ provider: 'google', code: 'auth-code-123', state: 'state-456', }); }); expect(oauthApi.handleOAuthCallback).toHaveBeenCalledWith( 'google', 'auth-code-123', 'state-456' ); expect(cookies.setCookie).toHaveBeenCalledWith('access_token', 'access-token-123', 7); expect(cookies.setCookie).toHaveBeenCalledWith('refresh_token', 'refresh-token-456', 7); }); it('sets user in cache after successful callback', async () => { const mockUser = { id: 1, username: 'testuser', email: 'test@example.com', name: 'Test User', role: 'owner', is_staff: false, is_superuser: false, }; const mockResponse: oauthApi.OAuthTokenResponse = { access: 'access-token', refresh: 'refresh-token', user: mockUser, }; vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useOAuthCallback(), { wrapper }); await act(async () => { await result.current.mutateAsync({ provider: 'google', code: 'code', state: 'state', }); }); // Verify user was set in cache const cachedUser = queryClient.getQueryData(['currentUser']); expect(cachedUser).toEqual(mockUser); }); it('invalidates OAuth connections after successful callback', async () => { const mockResponse: oauthApi.OAuthTokenResponse = { access: 'access-token', refresh: 'refresh-token', user: { id: 1, username: 'testuser', email: 'test@example.com', name: 'Test User', role: 'owner', is_staff: false, is_superuser: false, }, }; vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); // Set initial connections data queryClient.setQueryData(['oauthConnections'], []); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useOAuthCallback(), { wrapper }); await act(async () => { await result.current.mutateAsync({ provider: 'google', code: 'code', state: 'state', }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthConnections'] }); }); it('handles errors during OAuth callback', async () => { const mockError = new Error('Invalid authorization code'); vi.mocked(oauthApi.handleOAuthCallback).mockRejectedValue(mockError); const { result } = renderHook(() => useOAuthCallback(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync({ provider: 'google', code: 'invalid-code', state: 'state', }); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); expect(cookies.setCookie).not.toHaveBeenCalled(); }); it('handles callback with optional user fields', async () => { const mockResponse: oauthApi.OAuthTokenResponse = { access: 'access-token', refresh: 'refresh-token', user: { id: 1, username: 'testuser', email: 'test@example.com', name: 'Test User', role: 'owner', avatar_url: 'https://example.com/avatar.png', is_staff: true, is_superuser: false, business: 123, business_name: 'Test Business', business_subdomain: 'testbiz', }, }; vi.mocked(oauthApi.handleOAuthCallback).mockResolvedValue(mockResponse); const { result } = renderHook(() => useOAuthCallback(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ provider: 'microsoft', code: 'code', state: 'state', }); }); expect(cookies.setCookie).toHaveBeenCalledWith('access_token', 'access-token', 7); expect(cookies.setCookie).toHaveBeenCalledWith('refresh_token', 'refresh-token', 7); }); }); describe('useDisconnectOAuth', () => { it('disconnects OAuth provider successfully', async () => { vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined); const { result } = renderHook(() => useDisconnectOAuth(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync('google'); }); // React Query passes mutation context as second parameter expect(oauthApi.disconnectOAuth).toHaveBeenCalledWith('google', expect.any(Object)); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('invalidates OAuth connections after disconnect', async () => { vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDisconnectOAuth(), { wrapper }); await act(async () => { await result.current.mutateAsync('google'); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthConnections'] }); }); it('handles errors when disconnect fails', async () => { const mockError = new Error('Failed to disconnect'); vi.mocked(oauthApi.disconnectOAuth).mockRejectedValue(mockError); const { result } = renderHook(() => useDisconnectOAuth(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync('google'); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); it('can disconnect multiple providers sequentially', async () => { vi.mocked(oauthApi.disconnectOAuth).mockResolvedValue(undefined); const { result } = renderHook(() => useDisconnectOAuth(), { wrapper: createWrapper(), }); // Disconnect first provider await act(async () => { await result.current.mutateAsync('google'); }); // React Query passes mutation context as second parameter expect(oauthApi.disconnectOAuth).toHaveBeenNthCalledWith(1, 'google', expect.any(Object)); // Disconnect second provider await act(async () => { await result.current.mutateAsync('microsoft'); }); expect(oauthApi.disconnectOAuth).toHaveBeenNthCalledWith(2, 'microsoft', expect.any(Object)); expect(oauthApi.disconnectOAuth).toHaveBeenCalledTimes(2); }); }); });