import { describe, it, expect, vi, beforeEach } from 'vitest'; import apiClient from '../client'; import * as staffEmailApi from '../staffEmail'; vi.mock('../client'); describe('staffEmail API', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Folder Operations', () => { const mockFolderResponse = { id: 1, owner: 1, name: 'Inbox', folder_type: 'inbox', email_count: 10, unread_count: 3, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; describe('getFolders', () => { it('fetches all folders', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockFolderResponse] }); const result = await staffEmailApi.getFolders(); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/folders/'); expect(result).toHaveLength(1); expect(result[0].folderType).toBe('inbox'); expect(result[0].emailCount).toBe(10); }); it('transforms snake_case to camelCase', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockFolderResponse] }); const result = await staffEmailApi.getFolders(); expect(result[0].createdAt).toBe('2024-01-01T00:00:00Z'); expect(result[0].updatedAt).toBe('2024-01-01T00:00:00Z'); }); }); describe('createFolder', () => { it('creates a new folder', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { ...mockFolderResponse, id: 2, name: 'Custom' }, }); const result = await staffEmailApi.createFolder('Custom'); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/folders/', { name: 'Custom' }); expect(result.name).toBe('Custom'); }); }); describe('updateFolder', () => { it('updates a folder name', async () => { vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: { ...mockFolderResponse, name: 'Updated' }, }); const result = await staffEmailApi.updateFolder(1, 'Updated'); expect(apiClient.patch).toHaveBeenCalledWith('/staff-email/folders/1/', { name: 'Updated' }); expect(result.name).toBe('Updated'); }); }); describe('deleteFolder', () => { it('deletes a folder', async () => { vi.mocked(apiClient.delete).mockResolvedValueOnce({}); await staffEmailApi.deleteFolder(1); expect(apiClient.delete).toHaveBeenCalledWith('/staff-email/folders/1/'); }); }); }); describe('Email Operations', () => { const mockEmailResponse = { id: 1, folder: 1, from_address: 'sender@example.com', from_name: 'Sender', to_addresses: [{ email: 'recipient@example.com', name: 'Recipient' }], subject: 'Test Email', snippet: 'This is a test...', status: 'received', is_read: false, is_starred: false, is_important: false, has_attachments: false, attachment_count: 0, thread_id: 'thread-1', email_date: '2024-01-01T12:00:00Z', created_at: '2024-01-01T12:00:00Z', labels: [], }; describe('getEmails', () => { it('fetches emails with filters', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: { count: 1, next: null, previous: null, results: [mockEmailResponse], }, }); const result = await staffEmailApi.getEmails({ folderId: 1 }, 1, 50); expect(apiClient.get).toHaveBeenCalledWith( expect.stringContaining('/staff-email/messages/') ); expect(result.count).toBe(1); expect(result.results).toHaveLength(1); }); it('handles legacy array response', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockEmailResponse], }); const result = await staffEmailApi.getEmails({}, 1, 50); expect(result.count).toBe(1); expect(result.results).toHaveLength(1); }); it('applies all filter parameters', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: { count: 0, next: null, previous: null, results: [] }, }); await staffEmailApi.getEmails({ folderId: 1, emailAddressId: 2, isRead: true, isStarred: false, search: 'test', fromDate: '2024-01-01', toDate: '2024-01-31', }); const callUrl = vi.mocked(apiClient.get).mock.calls[0][0] as string; expect(callUrl).toContain('folder=1'); expect(callUrl).toContain('email_address=2'); expect(callUrl).toContain('is_read=true'); expect(callUrl).toContain('is_starred=false'); expect(callUrl).toContain('search=test'); expect(callUrl).toContain('from_date=2024-01-01'); expect(callUrl).toContain('to_date=2024-01-31'); }); }); describe('getEmail', () => { it('fetches a single email by id', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockEmailResponse }); const result = await staffEmailApi.getEmail(1); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/messages/1/'); expect(result.fromAddress).toBe('sender@example.com'); }); }); describe('getEmailThread', () => { it('fetches emails in a thread', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: { results: [mockEmailResponse] }, }); const result = await staffEmailApi.getEmailThread('thread-1'); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/messages/', { params: { thread_id: 'thread-1' }, }); expect(result).toHaveLength(1); }); }); }); describe('Draft Operations', () => { describe('createDraft', () => { it('creates a draft with formatted addresses', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 1, folder: 1, subject: 'New Draft', from_address: 'sender@example.com', to_addresses: [{ email: 'recipient@example.com', name: '' }], }, }); await staffEmailApi.createDraft({ emailAddressId: 1, toAddresses: ['recipient@example.com'], subject: 'New Draft', bodyText: 'Body text', }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/', expect.objectContaining({ email_address: 1, to_addresses: [{ email: 'recipient@example.com', name: '' }], subject: 'New Draft', })); }); it('handles "Name " format', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 1 }, }); await staffEmailApi.createDraft({ emailAddressId: 1, toAddresses: ['John Doe '], subject: 'Test', }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/', expect.objectContaining({ to_addresses: [{ email: 'john@example.com', name: 'John Doe' }], })); }); }); describe('updateDraft', () => { it('updates draft subject', async () => { vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: { id: 1, subject: 'Updated Subject' }, }); await staffEmailApi.updateDraft(1, { subject: 'Updated Subject' }); expect(apiClient.patch).toHaveBeenCalledWith('/staff-email/messages/1/', { subject: 'Updated Subject', }); }); }); describe('deleteDraft', () => { it('deletes a draft', async () => { vi.mocked(apiClient.delete).mockResolvedValueOnce({}); await staffEmailApi.deleteDraft(1); expect(apiClient.delete).toHaveBeenCalledWith('/staff-email/messages/1/'); }); }); }); describe('Send/Reply/Forward', () => { describe('sendEmail', () => { it('sends a draft', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 1, status: 'sent' }, }); await staffEmailApi.sendEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/send/'); }); }); describe('replyToEmail', () => { it('replies to an email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 2, in_reply_to: 1 }, }); await staffEmailApi.replyToEmail(1, { bodyText: 'Reply body', replyAll: false, }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/reply/'); }); }); describe('forwardEmail', () => { it('forwards an email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 3 }, }); await staffEmailApi.forwardEmail(1, { toAddresses: ['forward@example.com'], bodyText: 'FW: Original message', }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/forward/', expect.objectContaining({ to_addresses: [{ email: 'forward@example.com', name: '' }], })); }); }); }); describe('Email Actions', () => { describe('markAsRead', () => { it('marks email as read', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.markAsRead(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/mark_read/'); }); }); describe('markAsUnread', () => { it('marks email as unread', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.markAsUnread(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/mark_unread/'); }); }); describe('starEmail', () => { it('stars an email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.starEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/star/'); }); }); describe('unstarEmail', () => { it('unstars an email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.unstarEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/unstar/'); }); }); describe('archiveEmail', () => { it('archives an email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.archiveEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/archive/'); }); }); describe('trashEmail', () => { it('moves email to trash', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.trashEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/trash/'); }); }); describe('restoreEmail', () => { it('restores email from trash', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.restoreEmail(1); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/restore/'); }); }); describe('permanentlyDeleteEmail', () => { it('permanently deletes an email', async () => { vi.mocked(apiClient.delete).mockResolvedValueOnce({}); await staffEmailApi.permanentlyDeleteEmail(1); expect(apiClient.delete).toHaveBeenCalledWith('/staff-email/messages/1/'); }); }); describe('moveEmails', () => { it('moves emails to a folder', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.moveEmails({ emailIds: [1, 2, 3], folderId: 2 }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/move/', { email_ids: [1, 2, 3], folder_id: 2, }); }); }); describe('bulkAction', () => { it('performs bulk action on emails', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.bulkAction({ emailIds: [1, 2], action: 'mark_read' }); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/bulk_action/', { email_ids: [1, 2], action: 'mark_read', }); }); }); }); describe('Labels', () => { const mockLabelResponse = { id: 1, owner: 1, name: 'Important', color: '#ef4444', created_at: '2024-01-01T00:00:00Z', }; describe('getLabels', () => { it('fetches all labels', async () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ data: [mockLabelResponse] }); const result = await staffEmailApi.getLabels(); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/labels/'); expect(result).toHaveLength(1); expect(result[0].name).toBe('Important'); }); }); describe('createLabel', () => { it('creates a new label', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { ...mockLabelResponse, id: 2, name: 'Work', color: '#10b981' }, }); const result = await staffEmailApi.createLabel('Work', '#10b981'); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/labels/', { name: 'Work', color: '#10b981' }); expect(result.name).toBe('Work'); }); }); describe('updateLabel', () => { it('updates a label', async () => { vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: { ...mockLabelResponse, name: 'Updated' }, }); const result = await staffEmailApi.updateLabel(1, { name: 'Updated' }); expect(apiClient.patch).toHaveBeenCalledWith('/staff-email/labels/1/', { name: 'Updated' }); expect(result.name).toBe('Updated'); }); }); describe('deleteLabel', () => { it('deletes a label', async () => { vi.mocked(apiClient.delete).mockResolvedValueOnce({}); await staffEmailApi.deleteLabel(1); expect(apiClient.delete).toHaveBeenCalledWith('/staff-email/labels/1/'); }); }); describe('addLabelToEmail', () => { it('adds label to email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.addLabelToEmail(1, 2); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/add_label/', { label_id: 2 }); }); }); describe('removeLabelFromEmail', () => { it('removes label from email', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({}); await staffEmailApi.removeLabelFromEmail(1, 2); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/1/remove_label/', { label_id: 2 }); }); }); }); describe('Contacts', () => { describe('searchContacts', () => { it('searches contacts', async () => { const mockContacts = [ { id: 1, owner: 1, email: 'test@example.com', name: 'Test', use_count: 5, last_used_at: '2024-01-01' }, ]; vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockContacts }); const result = await staffEmailApi.searchContacts('test'); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/contacts/', { params: { search: 'test' }, }); expect(result[0].email).toBe('test@example.com'); expect(result[0].useCount).toBe(5); }); }); }); describe('Attachments', () => { describe('uploadAttachment', () => { it('uploads a file attachment', async () => { const mockResponse = { id: 1, filename: 'test.pdf', content_type: 'application/pdf', size: 1024, url: 'https://example.com/test.pdf', created_at: '2024-01-01T00:00:00Z', }; vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse }); const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }); const result = await staffEmailApi.uploadAttachment(file, 1); expect(apiClient.post).toHaveBeenCalledWith( '/staff-email/attachments/', expect.any(FormData), { headers: { 'Content-Type': 'multipart/form-data' } } ); expect(result.filename).toBe('test.pdf'); }); it('uploads attachment without email id', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { id: 1, filename: 'test.pdf' }, }); const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }); await staffEmailApi.uploadAttachment(file); expect(apiClient.post).toHaveBeenCalled(); }); }); describe('deleteAttachment', () => { it('deletes an attachment', async () => { vi.mocked(apiClient.delete).mockResolvedValueOnce({}); await staffEmailApi.deleteAttachment(1); expect(apiClient.delete).toHaveBeenCalledWith('/staff-email/attachments/1/'); }); }); }); describe('Sync', () => { describe('syncEmails', () => { it('triggers email sync', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { success: true, message: 'Synced' }, }); const result = await staffEmailApi.syncEmails(); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/sync/'); expect(result.success).toBe(true); }); }); describe('fullSyncEmails', () => { it('triggers full email sync', async () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { status: 'started', tasks: [{ email_address: 'user@example.com', task_id: 'task-1' }], }, }); const result = await staffEmailApi.fullSyncEmails(); expect(apiClient.post).toHaveBeenCalledWith('/staff-email/messages/full_sync/'); expect(result.status).toBe('started'); expect(result.tasks).toHaveLength(1); }); }); }); describe('User Email Addresses', () => { describe('getUserEmailAddresses', () => { it('fetches user email addresses', async () => { const mockAddresses = [ { id: 1, email_address: 'user@example.com', display_name: 'User', color: '#3b82f6', is_default: true, last_check_at: '2024-01-01T00:00:00Z', emails_processed_count: 100, }, ]; vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockAddresses }); const result = await staffEmailApi.getUserEmailAddresses(); expect(apiClient.get).toHaveBeenCalledWith('/staff-email/messages/email_addresses/'); expect(result).toHaveLength(1); expect(result[0].email_address).toBe('user@example.com'); }); }); }); });