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'; vi.mock('../../api/ticketEmailSettings', () => ({ getTicketEmailSettings: vi.fn(), updateTicketEmailSettings: vi.fn(), testImapConnection: vi.fn(), testSmtpConnection: vi.fn(), fetchEmailsNow: vi.fn(), getIncomingEmails: vi.fn(), reprocessIncomingEmail: vi.fn(), detectEmailProvider: vi.fn(), getOAuthStatus: vi.fn(), initiateGoogleOAuth: vi.fn(), initiateMicrosoftOAuth: vi.fn(), getOAuthCredentials: vi.fn(), deleteOAuthCredential: vi.fn(), })); import { useTicketEmailSettings, useUpdateTicketEmailSettings, useTestImapConnection, useTestSmtpConnection, useFetchEmailsNow, useIncomingEmails, useReprocessIncomingEmail, useDetectEmailProvider, useOAuthStatus, useInitiateGoogleOAuth, useInitiateMicrosoftOAuth, useOAuthCredentials, useDeleteOAuthCredential, } from '../useTicketEmailSettings'; import * as api from '../../api/ticketEmailSettings'; // Create wrapper 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('useTicketEmailSettings hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useTicketEmailSettings', () => { it('fetches ticket email settings', async () => { const mockSettings = { imap_host: 'imap.gmail.com', imap_port: 993, imap_use_ssl: true, imap_username: 'support@example.com', imap_password_masked: '***', imap_folder: 'INBOX', smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'support@example.com', smtp_password_masked: '***', smtp_from_email: 'support@example.com', smtp_from_name: 'Support Team', support_email_address: 'support@example.com', support_email_domain: 'example.com', is_enabled: true, delete_after_processing: false, check_interval_seconds: 300, max_attachment_size_mb: 10, allowed_attachment_types: ['pdf', 'png', 'jpg'], last_check_at: '2025-01-01T12:00:00Z', last_error: '', emails_processed_count: 42, is_configured: true, is_imap_configured: true, is_smtp_configured: true, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T12:00:00Z', }; vi.mocked(api.getTicketEmailSettings).mockResolvedValue(mockSettings); const { result } = renderHook(() => useTicketEmailSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getTicketEmailSettings).toHaveBeenCalledOnce(); expect(result.current.data).toEqual(mockSettings); expect(result.current.data?.imap_host).toBe('imap.gmail.com'); expect(result.current.data?.smtp_host).toBe('smtp.gmail.com'); }); it('handles error when fetching settings fails', async () => { vi.mocked(api.getTicketEmailSettings).mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => useTicketEmailSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeDefined(); }); }); describe('useUpdateTicketEmailSettings', () => { it('updates ticket email settings', async () => { const mockUpdate = { imap_host: 'imap.gmail.com', imap_port: 993, imap_use_ssl: true, }; const mockResponse = { ...mockUpdate, imap_username: 'support@example.com', imap_password_masked: '***', imap_folder: 'INBOX', smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'support@example.com', smtp_password_masked: '***', smtp_from_email: 'support@example.com', smtp_from_name: 'Support', support_email_address: 'support@example.com', support_email_domain: 'example.com', is_enabled: true, delete_after_processing: false, check_interval_seconds: 300, max_attachment_size_mb: 10, allowed_attachment_types: ['pdf'], last_check_at: null, last_error: '', emails_processed_count: 0, is_configured: true, is_imap_configured: true, is_smtp_configured: true, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T12:00:00Z', }; vi.mocked(api.updateTicketEmailSettings).mockResolvedValue(mockResponse); const { result } = renderHook(() => useUpdateTicketEmailSettings(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(mockUpdate); }); expect(api.updateTicketEmailSettings).toHaveBeenCalledWith(mockUpdate); expect(mutationResult).toEqual(mockResponse); }); it('invalidates query cache on successful update', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const mockResponse = { imap_host: 'imap.gmail.com', imap_port: 993, imap_use_ssl: true, imap_username: 'support@example.com', imap_password_masked: '***', imap_folder: 'INBOX', smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'support@example.com', smtp_password_masked: '***', smtp_from_email: 'support@example.com', smtp_from_name: 'Support', support_email_address: 'support@example.com', support_email_domain: 'example.com', is_enabled: true, delete_after_processing: false, check_interval_seconds: 300, max_attachment_size_mb: 10, allowed_attachment_types: [], last_check_at: null, last_error: '', emails_processed_count: 0, is_configured: true, is_imap_configured: true, is_smtp_configured: true, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T12:00:00Z', }; vi.mocked(api.updateTicketEmailSettings).mockResolvedValue(mockResponse); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateTicketEmailSettings(), { wrapper }); await act(async () => { await result.current.mutateAsync({ is_enabled: true }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailSettings'] }); }); it('handles validation errors', async () => { vi.mocked(api.updateTicketEmailSettings).mockRejectedValue( new Error('Invalid IMAP port') ); const { result } = renderHook(() => useUpdateTicketEmailSettings(), { wrapper: createWrapper(), }); let error; await act(async () => { try { await result.current.mutateAsync({ imap_port: -1 }); } catch (e) { error = e; } }); expect(error).toBeDefined(); expect(error).toBeInstanceOf(Error); }); }); describe('useTestImapConnection', () => { it('successfully tests IMAP connection', async () => { const mockResult = { success: true, message: 'IMAP connection successful', }; vi.mocked(api.testImapConnection).mockResolvedValue(mockResult); const { result } = renderHook(() => useTestImapConnection(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(api.testImapConnection).toHaveBeenCalledOnce(); expect(mutationResult).toEqual(mockResult); expect(mutationResult?.success).toBe(true); }); it('handles IMAP connection failure', async () => { const mockResult = { success: false, message: 'Authentication failed', }; vi.mocked(api.testImapConnection).mockResolvedValue(mockResult); const { result } = renderHook(() => useTestImapConnection(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(mutationResult?.success).toBe(false); expect(mutationResult?.message).toBe('Authentication failed'); }); }); describe('useTestSmtpConnection', () => { it('successfully tests SMTP connection', async () => { const mockResult = { success: true, message: 'SMTP connection successful', }; vi.mocked(api.testSmtpConnection).mockResolvedValue(mockResult); const { result } = renderHook(() => useTestSmtpConnection(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(api.testSmtpConnection).toHaveBeenCalledOnce(); expect(mutationResult).toEqual(mockResult); expect(mutationResult?.success).toBe(true); }); it('handles SMTP connection failure', async () => { const mockResult = { success: false, message: 'Connection timeout', }; vi.mocked(api.testSmtpConnection).mockResolvedValue(mockResult); const { result } = renderHook(() => useTestSmtpConnection(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(mutationResult?.success).toBe(false); expect(mutationResult?.message).toBe('Connection timeout'); }); }); describe('useFetchEmailsNow', () => { it('manually fetches emails', async () => { const mockResult = { success: true, message: 'Fetched 5 new emails', processed: 5, }; vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(api.fetchEmailsNow).toHaveBeenCalledOnce(); expect(mutationResult?.processed).toBe(5); }); it('invalidates queries after fetching emails', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const mockResult = { success: true, message: 'Fetched 3 emails', processed: 3, }; vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailSettings'] }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['incomingTicketEmails'] }); }); it('handles fetch error', async () => { const mockResult = { success: false, message: 'IMAP connection failed', processed: 0, }; vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(mutationResult?.success).toBe(false); expect(mutationResult?.processed).toBe(0); }); }); describe('useIncomingEmails', () => { it('fetches incoming emails without filters', async () => { const mockEmails = [ { id: 1, message_id: '', from_address: 'customer@example.com', from_name: 'John Doe', to_address: 'support@example.com', subject: 'Help with order', body_text: 'I need help with my order', extracted_reply: 'I need help', ticket: 101, ticket_subject: 'Help with order', matched_user: 42, ticket_id_from_email: '101', processing_status: 'PROCESSED' as const, processing_status_display: 'Processed', error_message: '', email_date: '2025-01-01T10:00:00Z', received_at: '2025-01-01T10:05:00Z', processed_at: '2025-01-01T10:06:00Z', }, ]; vi.mocked(api.getIncomingEmails).mockResolvedValue(mockEmails); const { result } = renderHook(() => useIncomingEmails(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getIncomingEmails).toHaveBeenCalledWith(undefined); expect(result.current.data).toHaveLength(1); expect(result.current.data?.[0].from_address).toBe('customer@example.com'); }); it('fetches incoming emails with status filter', async () => { const mockEmails = [ { id: 2, message_id: '', from_address: 'user@example.com', from_name: 'Jane Smith', to_address: 'support@example.com', subject: 'Failed email', body_text: 'Test', extracted_reply: 'Test', ticket: null, ticket_subject: '', matched_user: null, ticket_id_from_email: '', processing_status: 'FAILED' as const, processing_status_display: 'Failed', error_message: 'Could not parse ticket ID', email_date: '2025-01-01T11:00:00Z', received_at: '2025-01-01T11:05:00Z', processed_at: null, }, ]; vi.mocked(api.getIncomingEmails).mockResolvedValue(mockEmails); const { result } = renderHook(() => useIncomingEmails({ status: 'FAILED' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getIncomingEmails).toHaveBeenCalledWith({ status: 'FAILED' }); expect(result.current.data?.[0].processing_status).toBe('FAILED'); }); it('fetches incoming emails with ticket filter', async () => { vi.mocked(api.getIncomingEmails).mockResolvedValue([]); const { result } = renderHook(() => useIncomingEmails({ ticket: 101 }), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getIncomingEmails).toHaveBeenCalledWith({ ticket: 101 }); }); it('fetches incoming emails with multiple filters', async () => { vi.mocked(api.getIncomingEmails).mockResolvedValue([]); const { result } = renderHook( () => useIncomingEmails({ status: 'PROCESSED', ticket: 101 }), { wrapper: createWrapper() } ); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getIncomingEmails).toHaveBeenCalledWith({ status: 'PROCESSED', ticket: 101, }); }); }); describe('useReprocessIncomingEmail', () => { it('reprocesses failed incoming email', async () => { const mockResult = { success: true, message: 'Email reprocessed successfully', comment_id: 456, ticket_id: 789, }; vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult); const { result } = renderHook(() => useReprocessIncomingEmail(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(123); }); expect(api.reprocessIncomingEmail).toHaveBeenCalledWith(123); expect(mutationResult?.success).toBe(true); expect(mutationResult?.ticket_id).toBe(789); }); it('invalidates incoming emails cache on success', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const mockResult = { success: true, message: 'Reprocessed', }; vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useReprocessIncomingEmail(), { wrapper }); await act(async () => { await result.current.mutateAsync(123); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['incomingTicketEmails'] }); }); it('handles reprocessing failure', async () => { const mockResult = { success: false, message: 'Still cannot parse ticket ID', }; vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult); const { result } = renderHook(() => useReprocessIncomingEmail(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(123); }); expect(mutationResult?.success).toBe(false); }); }); describe('useDetectEmailProvider', () => { it('detects Gmail provider', async () => { const mockResult = { success: true, email: 'user@gmail.com', domain: 'gmail.com', detected: true, detected_via: 'domain_lookup' as const, provider: 'google' as const, display_name: 'Gmail', imap_host: 'imap.gmail.com', imap_port: 993, smtp_host: 'smtp.gmail.com', smtp_port: 587, oauth_supported: true, notes: 'OAuth recommended', }; vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult); const { result } = renderHook(() => useDetectEmailProvider(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync('user@gmail.com'); }); expect(api.detectEmailProvider).toHaveBeenCalledWith('user@gmail.com'); expect(mutationResult?.provider).toBe('google'); expect(mutationResult?.oauth_supported).toBe(true); }); it('detects Microsoft provider', async () => { const mockResult = { success: true, email: 'user@outlook.com', domain: 'outlook.com', detected: true, detected_via: 'domain_lookup' as const, provider: 'microsoft' as const, display_name: 'Outlook.com', imap_host: 'outlook.office365.com', imap_port: 993, smtp_host: 'smtp.office365.com', smtp_port: 587, oauth_supported: true, }; vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult); const { result } = renderHook(() => useDetectEmailProvider(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync('user@outlook.com'); }); expect(mutationResult?.provider).toBe('microsoft'); }); it('detects custom domain using MX records', async () => { const mockResult = { success: true, email: 'user@company.com', domain: 'company.com', detected: true, detected_via: 'mx_record' as const, provider: 'google' as const, display_name: 'Google Workspace', oauth_supported: true, message: 'Detected Google Workspace via MX records', }; vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult); const { result } = renderHook(() => useDetectEmailProvider(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync('user@company.com'); }); expect(mutationResult?.detected_via).toBe('mx_record'); expect(mutationResult?.provider).toBe('google'); }); it('handles unknown provider', async () => { const mockResult = { success: true, email: 'user@custom.com', domain: 'custom.com', detected: false, provider: 'unknown' as const, display_name: 'Unknown Provider', oauth_supported: false, message: 'Could not auto-detect provider', }; vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult); const { result } = renderHook(() => useDetectEmailProvider(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync('user@custom.com'); }); expect(mutationResult?.detected).toBe(false); expect(mutationResult?.provider).toBe('unknown'); }); }); describe('useOAuthStatus', () => { it('fetches OAuth status for both providers', async () => { const mockStatus = { google: { configured: true }, microsoft: { configured: false }, }; vi.mocked(api.getOAuthStatus).mockResolvedValue(mockStatus); const { result } = renderHook(() => useOAuthStatus(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getOAuthStatus).toHaveBeenCalledOnce(); expect(result.current.data?.google.configured).toBe(true); expect(result.current.data?.microsoft.configured).toBe(false); }); it('handles both providers not configured', async () => { const mockStatus = { google: { configured: false }, microsoft: { configured: false }, }; vi.mocked(api.getOAuthStatus).mockResolvedValue(mockStatus); const { result } = renderHook(() => useOAuthStatus(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.google.configured).toBe(false); expect(result.current.data?.microsoft.configured).toBe(false); }); }); describe('useInitiateGoogleOAuth', () => { it('initiates Google OAuth flow with default purpose', async () => { const mockResult = { success: true, authorization_url: 'https://accounts.google.com/o/oauth2/auth?...', }; vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateGoogleOAuth(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(api.initiateGoogleOAuth).toHaveBeenCalledWith('email'); expect(mutationResult?.success).toBe(true); expect(mutationResult?.authorization_url).toContain('google.com'); }); it('initiates Google OAuth flow with custom purpose', async () => { const mockResult = { success: true, authorization_url: 'https://accounts.google.com/o/oauth2/auth?...', }; vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateGoogleOAuth(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync('calendar'); }); expect(api.initiateGoogleOAuth).toHaveBeenCalledWith('calendar'); }); it('handles OAuth initiation error', async () => { const mockResult = { success: false, error: 'OAuth not configured', }; vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateGoogleOAuth(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(mutationResult?.success).toBe(false); expect(mutationResult?.error).toBe('OAuth not configured'); }); }); describe('useInitiateMicrosoftOAuth', () => { it('initiates Microsoft OAuth flow with default purpose', async () => { const mockResult = { success: true, authorization_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...', }; vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateMicrosoftOAuth(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(api.initiateMicrosoftOAuth).toHaveBeenCalledWith('email'); expect(mutationResult?.success).toBe(true); expect(mutationResult?.authorization_url).toContain('microsoft'); }); it('initiates Microsoft OAuth flow with custom purpose', async () => { const mockResult = { success: true, authorization_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...', }; vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateMicrosoftOAuth(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync('calendar'); }); expect(api.initiateMicrosoftOAuth).toHaveBeenCalledWith('calendar'); }); it('handles OAuth initiation error', async () => { const mockResult = { success: false, error: 'OAuth not configured', }; vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult); const { result } = renderHook(() => useInitiateMicrosoftOAuth(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(); }); expect(mutationResult?.success).toBe(false); expect(mutationResult?.error).toBe('OAuth not configured'); }); }); describe('useOAuthCredentials', () => { it('fetches OAuth credentials list', async () => { const mockCredentials = [ { id: 1, provider: 'google' as const, email: 'support@example.com', purpose: 'email', is_valid: true, is_expired: false, last_used_at: '2025-01-01T12:00:00Z', last_error: '', created_at: '2025-01-01T00:00:00Z', }, { id: 2, provider: 'microsoft' as const, email: 'admin@example.com', purpose: 'calendar', is_valid: false, is_expired: true, last_used_at: '2024-12-01T10:00:00Z', last_error: 'Token expired', created_at: '2024-11-01T00:00:00Z', }, ]; vi.mocked(api.getOAuthCredentials).mockResolvedValue(mockCredentials); const { result } = renderHook(() => useOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(api.getOAuthCredentials).toHaveBeenCalledOnce(); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0].provider).toBe('google'); expect(result.current.data?.[1].is_expired).toBe(true); }); it('handles empty credentials list', async () => { vi.mocked(api.getOAuthCredentials).mockResolvedValue([]); const { result } = renderHook(() => useOAuthCredentials(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toHaveLength(0); }); }); describe('useDeleteOAuthCredential', () => { it('deletes OAuth credential', async () => { const mockResult = { success: true, message: 'Credential deleted successfully', }; vi.mocked(api.deleteOAuthCredential).mockResolvedValue(mockResult); const { result } = renderHook(() => useDeleteOAuthCredential(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync(123); }); expect(api.deleteOAuthCredential).toHaveBeenCalledWith(123); expect(mutationResult?.success).toBe(true); }); it('invalidates credentials cache on successful delete', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const mockResult = { success: true, message: 'Deleted', }; vi.mocked(api.deleteOAuthCredential).mockResolvedValue(mockResult); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDeleteOAuthCredential(), { wrapper }); await act(async () => { await result.current.mutateAsync(123); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthCredentials'] }); }); it('handles delete error', async () => { vi.mocked(api.deleteOAuthCredential).mockRejectedValue( new Error('Credential not found') ); const { result } = renderHook(() => useDeleteOAuthCredential(), { wrapper: createWrapper(), }); let error; await act(async () => { try { await result.current.mutateAsync(999); } catch (e) { error = e; } }); expect(error).toBeDefined(); expect(error).toBeInstanceOf(Error); }); }); });