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 the business API vi.mock('../../api/business', () => ({ getBusinessOAuthSettings: vi.fn(), updateBusinessOAuthSettings: vi.fn(), })); import { useBusinessOAuthSettings, useUpdateBusinessOAuthSettings, } from '../useBusinessOAuth'; import * as businessApi from '../../api/business'; // Create wrapper for React Query const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return function Wrapper({ children }: { children: React.ReactNode }) { return React.createElement(QueryClientProvider, { client: queryClient }, children); }; }; describe('useBusinessOAuth hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useBusinessOAuthSettings', () => { it('fetches business OAuth settings successfully', async () => { const mockResponse = { settings: { enabledProviders: ['google', 'microsoft'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, ], }; vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); // Initially loading expect(result.current.isLoading).toBe(true); // Wait for success await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(businessApi.getBusinessOAuthSettings).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockResponse); expect(result.current.data?.settings.enabledProviders).toHaveLength(2); expect(result.current.data?.availableProviders).toHaveLength(2); }); it('handles empty enabled providers', async () => { const mockResponse = { settings: { enabledProviders: [], allowRegistration: false, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.settings.enabledProviders).toEqual([]); expect(result.current.data?.availableProviders).toHaveLength(1); }); it('handles custom credentials enabled', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: true, useCustomCredentials: true, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.settings.useCustomCredentials).toBe(true); expect(result.current.data?.settings.autoLinkByEmail).toBe(true); }); it('handles API error gracefully', async () => { const mockError = new Error('Failed to fetch OAuth settings'); vi.mocked(businessApi.getBusinessOAuthSettings).mockRejectedValue(mockError); const { result } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); expect(result.current.data).toBeUndefined(); }); it('does not retry on failure', async () => { vi.mocked(businessApi.getBusinessOAuthSettings).mockRejectedValue( new Error('404 Not Found') ); const { result } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); // Should be called only once (no retries) expect(businessApi.getBusinessOAuthSettings).toHaveBeenCalledTimes(1); }); it('caches data with 5 minute stale time', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result, rerender } = renderHook(() => useBusinessOAuthSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Rerender should use cached data (within stale time) rerender(); // Should still only be called once expect(businessApi.getBusinessOAuthSettings).toHaveBeenCalledTimes(1); }); }); describe('useUpdateBusinessOAuthSettings', () => { it('updates enabled providers successfully', async () => { const mockResponse = { settings: { enabledProviders: ['google', 'microsoft', 'github'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, { id: 'github', name: 'GitHub', icon: 'github-icon', description: 'Sign in with GitHub', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ enabledProviders: ['google', 'microsoft', 'github'], }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ enabledProviders: ['google', 'microsoft', 'github'], }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('updates allowRegistration flag', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: false, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ allowRegistration: false, }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ allowRegistration: false, }); }); it('updates autoLinkByEmail flag', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: true, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ autoLinkByEmail: true, }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ autoLinkByEmail: true, }); }); it('updates useCustomCredentials flag', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: true, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ useCustomCredentials: true, }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ useCustomCredentials: true, }); }); it('updates multiple settings at once', async () => { const mockResponse = { settings: { enabledProviders: ['google', 'microsoft'], allowRegistration: false, autoLinkByEmail: true, useCustomCredentials: true, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ enabledProviders: ['google', 'microsoft'], allowRegistration: false, autoLinkByEmail: true, useCustomCredentials: true, }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ enabledProviders: ['google', 'microsoft'], allowRegistration: false, autoLinkByEmail: true, useCustomCredentials: true, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.settings.enabledProviders).toHaveLength(2); expect(result.current.data?.settings.allowRegistration).toBe(false); expect(result.current.data?.settings.autoLinkByEmail).toBe(true); expect(result.current.data?.settings.useCustomCredentials).toBe(true); }); it('updates query cache on success', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper, }); await act(async () => { await result.current.mutateAsync({ enabledProviders: ['google'], }); }); // Verify cache was updated const cachedData = queryClient.getQueryData(['businessOAuthSettings']); expect(cachedData).toEqual(mockResponse); }); it('handles update error gracefully', async () => { const mockError = new Error('Failed to update settings'); vi.mocked(businessApi.updateBusinessOAuthSettings).mockRejectedValue(mockError); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); let caughtError: any = null; await act(async () => { try { await result.current.mutateAsync({ allowRegistration: true, }); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); it('handles partial update with only enabledProviders', async () => { const mockResponse = { settings: { enabledProviders: ['github'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'github', name: 'GitHub', icon: 'github-icon', description: 'Sign in with GitHub', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ enabledProviders: ['github'], }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ enabledProviders: ['github'], }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.settings.enabledProviders).toEqual(['github']); }); it('handles empty enabled providers array', async () => { const mockResponse = { settings: { enabledProviders: [], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ enabledProviders: [], }); }); expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({ enabledProviders: [], }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.settings.enabledProviders).toEqual([]); }); it('preserves availableProviders from backend response', async () => { const mockResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, { id: 'github', name: 'GitHub', icon: 'github-icon', description: 'Sign in with GitHub', }, ], }; vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ enabledProviders: ['google'], }); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.availableProviders).toHaveLength(3); expect(result.current.data?.availableProviders.map(p => p.id)).toEqual([ 'google', 'microsoft', 'github', ]); }); }); describe('integration tests', () => { it('fetches settings then updates them', async () => { const initialResponse = { settings: { enabledProviders: ['google'], allowRegistration: true, autoLinkByEmail: false, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, ], }; const updatedResponse = { settings: { enabledProviders: ['google', 'microsoft'], allowRegistration: true, autoLinkByEmail: true, useCustomCredentials: false, }, availableProviders: [ { id: 'google', name: 'Google', icon: 'google-icon', description: 'Sign in with Google', }, { id: 'microsoft', name: 'Microsoft', icon: 'microsoft-icon', description: 'Sign in with Microsoft', }, ], }; vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(initialResponse); vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(updatedResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); // Fetch initial settings const { result: fetchResult } = renderHook(() => useBusinessOAuthSettings(), { wrapper, }); await waitFor(() => { expect(fetchResult.current.isSuccess).toBe(true); }); expect(fetchResult.current.data?.settings.enabledProviders).toEqual(['google']); expect(fetchResult.current.data?.settings.autoLinkByEmail).toBe(false); // Update settings const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthSettings(), { wrapper, }); await act(async () => { await updateResult.current.mutateAsync({ enabledProviders: ['google', 'microsoft'], autoLinkByEmail: true, }); }); // Verify cache was updated const cachedData = queryClient.getQueryData(['businessOAuthSettings']); expect(cachedData).toEqual(updatedResponse); }); }); });