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', () => ({ getBusinessOAuthCredentials: vi.fn(), updateBusinessOAuthCredentials: vi.fn(), })); import { useBusinessOAuthCredentials, useUpdateBusinessOAuthCredentials, } from '../useBusinessOAuthCredentials'; 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('useBusinessOAuthCredentials hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useBusinessOAuthCredentials', () => { it('fetches business OAuth credentials successfully', async () => { const mockResponse = { credentials: { google: { client_id: 'google-client-id-123', client_secret: 'google-client-secret-456', has_secret: true, }, microsoft: { client_id: 'microsoft-client-id-789', client_secret: 'microsoft-client-secret-012', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); // Initially loading expect(result.current.isLoading).toBe(true); // Wait for success await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(businessApi.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockResponse); expect(result.current.data?.credentials.google.client_id).toBe('google-client-id-123'); expect(result.current.data?.credentials.google.has_secret).toBe(true); expect(result.current.data?.credentials.microsoft.client_id).toBe('microsoft-client-id-789'); expect(result.current.data?.useCustomCredentials).toBe(true); }); it('handles empty credentials', async () => { const mockResponse = { credentials: {}, useCustomCredentials: false, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.credentials).toEqual({}); expect(result.current.data?.useCustomCredentials).toBe(false); }); it('handles credentials with has_secret false', async () => { const mockResponse = { credentials: { google: { client_id: 'google-client-id-123', client_secret: '', has_secret: false, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.credentials.google.has_secret).toBe(false); expect(result.current.data?.credentials.google.client_secret).toBe(''); }); it('handles multiple providers with mixed credential states', async () => { const mockResponse = { credentials: { google: { client_id: 'google-client-id', client_secret: 'google-secret', has_secret: true, }, microsoft: { client_id: 'microsoft-client-id', client_secret: '', has_secret: false, }, github: { client_id: '', client_secret: '', has_secret: false, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(Object.keys(result.current.data?.credentials || {})).toHaveLength(3); expect(result.current.data?.credentials.google.has_secret).toBe(true); expect(result.current.data?.credentials.microsoft.has_secret).toBe(false); expect(result.current.data?.credentials.github.has_secret).toBe(false); }); it('handles API error gracefully', async () => { const mockError = new Error('Failed to fetch OAuth credentials'); vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useBusinessOAuthCredentials(), { 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 (404)', async () => { vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue( new Error('404 Not Found') ); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); // Should be called only once (no retries) expect(businessApi.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1); }); it('caches data with 5 minute stale time', async () => { const mockResponse = { credentials: { google: { client_id: 'google-client-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result, rerender } = renderHook(() => useBusinessOAuthCredentials(), { 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.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1); }); it('handles 401 unauthorized error', async () => { const mockError = new Error('401 Unauthorized'); vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); it('handles network error', async () => { const mockError = new Error('Network Error'); vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); }); describe('useUpdateBusinessOAuthCredentials', () => { it('updates credentials for a single provider successfully', async () => { const mockResponse = { credentials: { google: { client_id: 'new-google-client-id', client_secret: 'new-google-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'new-google-client-id', client_secret: 'new-google-secret', }, }, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_id: 'new-google-client-id', client_secret: 'new-google-secret', }, }, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.credentials.google.client_id).toBe('new-google-client-id'); expect(result.current.data?.credentials.google.has_secret).toBe(true); }); it('updates credentials for multiple providers', async () => { const mockResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, microsoft: { client_id: 'microsoft-id', client_secret: 'microsoft-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', }, microsoft: { client_id: 'microsoft-id', client_secret: 'microsoft-secret', }, }, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', }, microsoft: { client_id: 'microsoft-id', client_secret: 'microsoft-secret', }, }, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(Object.keys(result.current.data?.credentials || {})).toHaveLength(2); }); it('updates only client_id without client_secret', async () => { const mockResponse = { credentials: { google: { client_id: 'updated-google-id', client_secret: 'existing-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'updated-google-id', }, }, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_id: 'updated-google-id', }, }, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('updates only client_secret without client_id', async () => { const mockResponse = { credentials: { google: { client_id: 'existing-google-id', client_secret: 'new-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_secret: 'new-secret', }, }, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_secret: 'new-secret', }, }, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('updates useCustomCredentials flag only', async () => { const mockResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: false, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ useCustomCredentials: false, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ useCustomCredentials: false, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.useCustomCredentials).toBe(false); }); it('updates both credentials and useCustomCredentials flag', async () => { const mockResponse = { credentials: { google: { client_id: 'custom-google-id', client_secret: 'custom-google-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'custom-google-id', client_secret: 'custom-google-secret', }, }, useCustomCredentials: true, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_id: 'custom-google-id', client_secret: 'custom-google-secret', }, }, useCustomCredentials: true, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.useCustomCredentials).toBe(true); expect(result.current.data?.credentials.google.has_secret).toBe(true); }); it('updates query cache on success', async () => { const mockResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).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(() => useUpdateBusinessOAuthCredentials(), { wrapper, }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', }, }, }); }); // Verify cache was updated const cachedData = queryClient.getQueryData(['businessOAuthCredentials']); expect(cachedData).toEqual(mockResponse); }); it('handles update error gracefully', async () => { const mockError = new Error('Failed to update credentials'); vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); let caughtError: any = null; await act(async () => { try { await result.current.mutateAsync({ credentials: { google: { client_id: 'test-id', }, }, }); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toEqual(mockError); }); it('handles validation error from API', async () => { const mockError = new Error('Invalid client_id format'); vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); let caughtError: any = null; await act(async () => { try { await result.current.mutateAsync({ credentials: { google: { client_id: 'invalid-format', }, }, }); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); it('handles clearing credentials by passing empty values', async () => { const mockResponse = { credentials: { google: { client_id: '', client_secret: '', has_secret: false, }, }, useCustomCredentials: false, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: '', client_secret: '', }, }, useCustomCredentials: false, }); }); expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({ credentials: { google: { client_id: '', client_secret: '', }, }, useCustomCredentials: false, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.credentials.google.has_secret).toBe(false); expect(result.current.data?.useCustomCredentials).toBe(false); }); it('handles permission error (403)', async () => { const mockError = new Error('403 Forbidden - Insufficient permissions'); vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); let caughtError: any = null; await act(async () => { try { await result.current.mutateAsync({ useCustomCredentials: true, }); } catch (error) { caughtError = error; } }); expect(caughtError).toEqual(mockError); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); it('preserves backend response structure with has_secret flags', async () => { const mockResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, microsoft: { client_id: 'microsoft-id', client_secret: '', has_secret: false, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', }, microsoft: { client_id: 'microsoft-id', }, }, }); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.credentials.google.has_secret).toBe(true); expect(result.current.data?.credentials.microsoft.has_secret).toBe(false); expect(result.current.data?.credentials.microsoft.client_secret).toBe(''); }); }); describe('integration tests', () => { it('fetches credentials then updates them', async () => { const initialResponse = { credentials: { google: { client_id: 'initial-google-id', client_secret: 'initial-google-secret', has_secret: true, }, }, useCustomCredentials: true, }; const updatedResponse = { credentials: { google: { client_id: 'updated-google-id', client_secret: 'updated-google-secret', has_secret: true, }, microsoft: { client_id: 'new-microsoft-id', client_secret: 'new-microsoft-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(initialResponse); vi.mocked(businessApi.updateBusinessOAuthCredentials).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 credentials const { result: fetchResult } = renderHook(() => useBusinessOAuthCredentials(), { wrapper, }); await waitFor(() => { expect(fetchResult.current.isSuccess).toBe(true); }); expect(fetchResult.current.data?.credentials.google.client_id).toBe('initial-google-id'); expect(Object.keys(fetchResult.current.data?.credentials || {})).toHaveLength(1); // Update credentials const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper, }); await act(async () => { await updateResult.current.mutateAsync({ credentials: { google: { client_id: 'updated-google-id', client_secret: 'updated-google-secret', }, microsoft: { client_id: 'new-microsoft-id', client_secret: 'new-microsoft-secret', }, }, }); }); // Verify cache was updated const cachedData = queryClient.getQueryData(['businessOAuthCredentials']); expect(cachedData).toEqual(updatedResponse); expect((cachedData as any).credentials.google.client_id).toBe('updated-google-id'); expect((cachedData as any).credentials.microsoft.client_id).toBe('new-microsoft-id'); }); it('toggles custom credentials on and off', async () => { const initialResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: true, }; const toggledOffResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: false, }; const toggledOnResponse = { credentials: { google: { client_id: 'google-id', client_secret: 'google-secret', has_secret: true, }, }, useCustomCredentials: true, }; vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(initialResponse); 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 state const { result: fetchResult } = renderHook(() => useBusinessOAuthCredentials(), { wrapper, }); await waitFor(() => { expect(fetchResult.current.isSuccess).toBe(true); }); expect(fetchResult.current.data?.useCustomCredentials).toBe(true); // Toggle off const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthCredentials(), { wrapper, }); vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(toggledOffResponse); await act(async () => { await updateResult.current.mutateAsync({ useCustomCredentials: false, }); }); let cachedData = queryClient.getQueryData(['businessOAuthCredentials']); expect((cachedData as any).useCustomCredentials).toBe(false); // Toggle back on vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(toggledOnResponse); await act(async () => { await updateResult.current.mutateAsync({ useCustomCredentials: true, }); }); cachedData = queryClient.getQueryData(['businessOAuthCredentials']); expect((cachedData as any).useCustomCredentials).toBe(true); }); }); });