Files
smoothschedule/frontend/src/api/__tests__/staffEmail.test.ts
poduck 416cd7059b Add global navigation search, cancellation policies, and UI improvements
- Add global search in top bar for navigating to dashboard pages
- Add cancellation policy settings (window hours, late fee, deposit refund)
- Display booking policies on customer confirmation page
- Filter API tokens by sandbox/live mode
- Widen settings layout and full-width site builder
- Add help documentation search with OpenAI integration
- Add blocked time ranges API for calendar visualization
- Update business hours settings with holiday management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 23:39:07 -05:00

612 lines
19 KiB
TypeScript

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 <email>" format', async () => {
vi.mocked(apiClient.post).mockResolvedValueOnce({
data: { id: 1 },
});
await staffEmailApi.createDraft({
emailAddressId: 1,
toAddresses: ['John Doe <john@example.com>'],
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');
});
});
});
});