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 ticket email addresses API module vi.mock('../../api/ticketEmailAddresses', () => ({ getTicketEmailAddresses: vi.fn(), getTicketEmailAddress: vi.fn(), createTicketEmailAddress: vi.fn(), updateTicketEmailAddress: vi.fn(), deleteTicketEmailAddress: vi.fn(), testImapConnection: vi.fn(), testSmtpConnection: vi.fn(), fetchEmailsNow: vi.fn(), setAsDefault: vi.fn(), })); import { useTicketEmailAddresses, useTicketEmailAddress, useCreateTicketEmailAddress, useUpdateTicketEmailAddress, useDeleteTicketEmailAddress, useTestImapConnection, useTestSmtpConnection, useFetchEmailsNow, useSetAsDefault, } from '../useTicketEmailAddresses'; import * as ticketEmailAddressesApi from '../../api/ticketEmailAddresses'; // Create wrapper with QueryClient 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('useTicketEmailAddresses hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useTicketEmailAddresses', () => { it('fetches all ticket email addresses', async () => { const mockAddresses = [ { id: 1, display_name: 'Support', email_address: 'support@example.com', color: '#FF5733', is_active: true, is_default: true, last_check_at: '2025-12-07T10:00:00Z', emails_processed_count: 42, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }, { id: 2, display_name: 'Sales', email_address: 'sales@example.com', color: '#33A1FF', is_active: true, is_default: false, last_check_at: null, emails_processed_count: 0, created_at: '2025-12-02T10:00:00Z', updated_at: '2025-12-02T10:00:00Z', }, ]; vi.mocked(ticketEmailAddressesApi.getTicketEmailAddresses).mockResolvedValue( mockAddresses as any ); const { result } = renderHook(() => useTicketEmailAddresses(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketEmailAddressesApi.getTicketEmailAddresses).toHaveBeenCalled(); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0]).toEqual(mockAddresses[0]); expect(result.current.data?.[1]).toEqual(mockAddresses[1]); }); it('handles empty list', async () => { vi.mocked(ticketEmailAddressesApi.getTicketEmailAddresses).mockResolvedValue([]); const { result } = renderHook(() => useTicketEmailAddresses(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); }); it('handles API errors', async () => { vi.mocked(ticketEmailAddressesApi.getTicketEmailAddresses).mockRejectedValue( new Error('API Error') ); const { result } = renderHook(() => useTicketEmailAddresses(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeInstanceOf(Error); }); }); describe('useTicketEmailAddress', () => { it('fetches single ticket email address by id', async () => { const mockAddress = { id: 1, tenant: 5, tenant_name: 'Example Business', display_name: 'Support', email_address: 'support@example.com', color: '#FF5733', imap_host: 'imap.gmail.com', imap_port: 993, imap_use_ssl: true, imap_username: 'support@example.com', imap_folder: 'INBOX', smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'support@example.com', is_active: true, is_default: true, last_check_at: '2025-12-07T10:00:00Z', last_error: null, emails_processed_count: 42, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', is_imap_configured: true, is_smtp_configured: true, is_fully_configured: true, }; vi.mocked(ticketEmailAddressesApi.getTicketEmailAddress).mockResolvedValue( mockAddress as any ); const { result } = renderHook(() => useTicketEmailAddress(1), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketEmailAddressesApi.getTicketEmailAddress).toHaveBeenCalledWith(1); expect(result.current.data).toEqual(mockAddress); }); it('does not fetch when id is 0', async () => { const { result } = renderHook(() => useTicketEmailAddress(0), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketEmailAddressesApi.getTicketEmailAddress).not.toHaveBeenCalled(); }); it('handles API errors', async () => { vi.mocked(ticketEmailAddressesApi.getTicketEmailAddress).mockRejectedValue( new Error('Not found') ); const { result } = renderHook(() => useTicketEmailAddress(999), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeInstanceOf(Error); }); it('handles addresses with last_error', async () => { const mockAddress = { id: 3, tenant: 5, tenant_name: 'Example Business', display_name: 'Broken Email', email_address: 'broken@example.com', color: '#FF0000', imap_host: 'imap.example.com', imap_port: 993, imap_use_ssl: true, imap_username: 'broken@example.com', imap_folder: 'INBOX', smtp_host: 'smtp.example.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'broken@example.com', is_active: true, is_default: false, last_check_at: '2025-12-07T10:00:00Z', last_error: 'Authentication failed', emails_processed_count: 0, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', is_imap_configured: true, is_smtp_configured: true, is_fully_configured: true, }; vi.mocked(ticketEmailAddressesApi.getTicketEmailAddress).mockResolvedValue( mockAddress as any ); const { result } = renderHook(() => useTicketEmailAddress(3), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.last_error).toBe('Authentication failed'); }); }); describe('useCreateTicketEmailAddress', () => { it('creates a new ticket email address', async () => { const newAddress = { display_name: 'Info', email_address: 'info@example.com', color: '#00FF00', imap_host: 'imap.gmail.com', imap_port: 993, imap_use_ssl: true, imap_username: 'info@example.com', imap_password: 'password123', imap_folder: 'INBOX', smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'info@example.com', smtp_password: 'password123', is_active: true, is_default: false, }; const mockResponse = { id: 10, ...newAddress }; vi.mocked(ticketEmailAddressesApi.createTicketEmailAddress).mockResolvedValue( mockResponse as any ); const { result } = renderHook(() => useCreateTicketEmailAddress(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(newAddress); }); expect(ticketEmailAddressesApi.createTicketEmailAddress).toHaveBeenCalledWith(newAddress); }); it('invalidates query cache on success', async () => { const newAddress = { display_name: 'Test', email_address: 'test@example.com', color: '#0000FF', imap_host: 'imap.example.com', imap_port: 993, imap_use_ssl: true, imap_username: 'test@example.com', imap_password: 'pass', imap_folder: 'INBOX', smtp_host: 'smtp.example.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'test@example.com', smtp_password: 'pass', is_active: true, is_default: false, }; const mockResponse = { id: 11, ...newAddress }; vi.mocked(ticketEmailAddressesApi.createTicketEmailAddress).mockResolvedValue( mockResponse as any ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useCreateTicketEmailAddress(), { wrapper }); await act(async () => { await result.current.mutateAsync(newAddress); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses'] }); }); it('handles creation errors', async () => { const newAddress = { display_name: 'Error', email_address: 'error@example.com', color: '#FF0000', imap_host: 'imap.example.com', imap_port: 993, imap_use_ssl: true, imap_username: 'error@example.com', imap_password: 'pass', imap_folder: 'INBOX', smtp_host: 'smtp.example.com', smtp_port: 587, smtp_use_tls: true, smtp_use_ssl: false, smtp_username: 'error@example.com', smtp_password: 'pass', is_active: true, is_default: false, }; vi.mocked(ticketEmailAddressesApi.createTicketEmailAddress).mockRejectedValue( new Error('Validation error') ); const { result } = renderHook(() => useCreateTicketEmailAddress(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(newAddress); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useUpdateTicketEmailAddress', () => { it('updates an existing ticket email address', async () => { const updates = { display_name: 'Updated Support', color: '#FF00FF', }; const mockResponse = { id: 1, ...updates }; vi.mocked(ticketEmailAddressesApi.updateTicketEmailAddress).mockResolvedValue( mockResponse as any ); const { result } = renderHook(() => useUpdateTicketEmailAddress(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: 1, data: updates }); }); expect(ticketEmailAddressesApi.updateTicketEmailAddress).toHaveBeenCalledWith(1, updates); }); it('updates email configuration', async () => { const updates = { imap_host: 'imap.newserver.com', imap_port: 993, imap_password: 'newpassword', }; const mockResponse = { id: 2, ...updates }; vi.mocked(ticketEmailAddressesApi.updateTicketEmailAddress).mockResolvedValue( mockResponse as any ); const { result } = renderHook(() => useUpdateTicketEmailAddress(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: 2, data: updates }); }); expect(ticketEmailAddressesApi.updateTicketEmailAddress).toHaveBeenCalledWith(2, updates); }); it('invalidates queries on success', async () => { const updates = { display_name: 'New Name' }; const mockResponse = { id: 3, ...updates }; vi.mocked(ticketEmailAddressesApi.updateTicketEmailAddress).mockResolvedValue( mockResponse as any ); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateTicketEmailAddress(), { wrapper }); await act(async () => { await result.current.mutateAsync({ id: 3, data: updates }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses'] }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses', 3] }); }); it('handles update errors', async () => { vi.mocked(ticketEmailAddressesApi.updateTicketEmailAddress).mockRejectedValue( new Error('Update failed') ); const { result } = renderHook(() => useUpdateTicketEmailAddress(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync({ id: 999, data: { display_name: 'Test' } }); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useDeleteTicketEmailAddress', () => { it('deletes a ticket email address', async () => { vi.mocked(ticketEmailAddressesApi.deleteTicketEmailAddress).mockResolvedValue(undefined); const { result } = renderHook(() => useDeleteTicketEmailAddress(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(5); }); expect(ticketEmailAddressesApi.deleteTicketEmailAddress).toHaveBeenCalledWith(5); }); it('invalidates query cache on success', async () => { vi.mocked(ticketEmailAddressesApi.deleteTicketEmailAddress).mockResolvedValue(undefined); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDeleteTicketEmailAddress(), { wrapper }); await act(async () => { await result.current.mutateAsync(6); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses'] }); }); it('handles deletion errors', async () => { vi.mocked(ticketEmailAddressesApi.deleteTicketEmailAddress).mockRejectedValue( new Error('Cannot delete default address') ); const { result } = renderHook(() => useDeleteTicketEmailAddress(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(1); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useTestImapConnection', () => { it('tests IMAP connection successfully', async () => { const mockResponse = { success: true, message: 'IMAP connection successful', }; vi.mocked(ticketEmailAddressesApi.testImapConnection).mockResolvedValue(mockResponse); const { result } = renderHook(() => useTestImapConnection(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(1); expect(response).toEqual(mockResponse); }); expect(ticketEmailAddressesApi.testImapConnection).toHaveBeenCalledWith(1); }); it('handles IMAP connection failure', async () => { const mockResponse = { success: false, message: 'Authentication failed', }; vi.mocked(ticketEmailAddressesApi.testImapConnection).mockResolvedValue(mockResponse); const { result } = renderHook(() => useTestImapConnection(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(2); expect(response.success).toBe(false); expect(response.message).toBe('Authentication failed'); }); }); it('handles API errors during IMAP test', async () => { vi.mocked(ticketEmailAddressesApi.testImapConnection).mockRejectedValue( new Error('Network error') ); const { result } = renderHook(() => useTestImapConnection(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(3); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useTestSmtpConnection', () => { it('tests SMTP connection successfully', async () => { const mockResponse = { success: true, message: 'SMTP connection successful', }; vi.mocked(ticketEmailAddressesApi.testSmtpConnection).mockResolvedValue(mockResponse); const { result } = renderHook(() => useTestSmtpConnection(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(1); expect(response).toEqual(mockResponse); }); expect(ticketEmailAddressesApi.testSmtpConnection).toHaveBeenCalledWith(1); }); it('handles SMTP connection failure', async () => { const mockResponse = { success: false, message: 'Could not connect to SMTP server', }; vi.mocked(ticketEmailAddressesApi.testSmtpConnection).mockResolvedValue(mockResponse); const { result } = renderHook(() => useTestSmtpConnection(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(2); expect(response.success).toBe(false); expect(response.message).toBe('Could not connect to SMTP server'); }); }); it('handles API errors during SMTP test', async () => { vi.mocked(ticketEmailAddressesApi.testSmtpConnection).mockRejectedValue( new Error('Timeout') ); const { result } = renderHook(() => useTestSmtpConnection(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(3); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useFetchEmailsNow', () => { it('fetches emails successfully', async () => { const mockResponse = { success: true, message: 'Successfully fetched 5 new emails', processed: 5, errors: 0, }; vi.mocked(ticketEmailAddressesApi.fetchEmailsNow).mockResolvedValue(mockResponse); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(1); expect(response).toEqual(mockResponse); }); expect(ticketEmailAddressesApi.fetchEmailsNow).toHaveBeenCalledWith(1); }); it('handles no new emails', async () => { const mockResponse = { success: true, message: 'No new emails', processed: 0, errors: 0, }; vi.mocked(ticketEmailAddressesApi.fetchEmailsNow).mockResolvedValue(mockResponse); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(2); expect(response.processed).toBe(0); }); }); it('handles errors during email fetch', async () => { const mockResponse = { success: false, message: 'Failed to fetch emails', processed: 2, errors: 3, }; vi.mocked(ticketEmailAddressesApi.fetchEmailsNow).mockResolvedValue(mockResponse); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(3); expect(response.success).toBe(false); expect(response.errors).toBe(3); }); }); it('invalidates queries on success', async () => { const mockResponse = { success: true, message: 'Fetched 3 emails', processed: 3, errors: 0, }; vi.mocked(ticketEmailAddressesApi.fetchEmailsNow).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper }); await act(async () => { await result.current.mutateAsync(4); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses'] }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets'] }); }); it('handles API errors during fetch', async () => { vi.mocked(ticketEmailAddressesApi.fetchEmailsNow).mockRejectedValue( new Error('Connection timeout') ); const { result } = renderHook(() => useFetchEmailsNow(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(5); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); }); describe('useSetAsDefault', () => { it('sets email address as default successfully', async () => { const mockResponse = { success: true, message: 'Email address set as default', }; vi.mocked(ticketEmailAddressesApi.setAsDefault).mockResolvedValue(mockResponse); const { result } = renderHook(() => useSetAsDefault(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(1); expect(response).toEqual(mockResponse); }); expect(ticketEmailAddressesApi.setAsDefault).toHaveBeenCalledWith(1); }); it('invalidates query cache on success', async () => { const mockResponse = { success: true, message: 'Email address set as default', }; vi.mocked(ticketEmailAddressesApi.setAsDefault).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useSetAsDefault(), { wrapper }); await act(async () => { await result.current.mutateAsync(2); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailAddresses'] }); }); it('handles errors when setting default', async () => { vi.mocked(ticketEmailAddressesApi.setAsDefault).mockRejectedValue( new Error('Cannot set inactive address as default') ); const { result } = renderHook(() => useSetAsDefault(), { wrapper: createWrapper(), }); let caughtError; await act(async () => { try { await result.current.mutateAsync(3); } catch (error) { caughtError = error; } }); expect(caughtError).toBeInstanceOf(Error); }); it('handles setting already default address', async () => { const mockResponse = { success: true, message: 'Email address is already the default', }; vi.mocked(ticketEmailAddressesApi.setAsDefault).mockResolvedValue(mockResponse); const { result } = renderHook(() => useSetAsDefault(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(1); expect(response.message).toBe('Email address is already the default'); }); }); }); });