- 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>
612 lines
19 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|
|
});
|