Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
8.9 KiB
8.9 KiB
Email Test Patterns
This guide explains the abstracted patterns for writing email E2E tests that are maintainable, reliable, and easy to understand.
Overview
Email testing involves complex tenant synchronization, workflow processing, and database state management. Rather than repeating this complexity in every test, we've abstracted common patterns into reusable utilities.
Quick Start
Basic Email Test
import { createPersistentE2EHelpers } from './utils/persistent-test-context';
import { createEmailTestHelpers } from './utils/email-test-helpers';
describe('My Email Tests', () => {
let context, emailHelpers;
beforeAll(async () => {
context = await createPersistentE2EHelpers().beforeAll();
emailHelpers = createEmailTestHelpers(context);
});
it('should process an email', async () => {
// Arrange - Create scenario with automatic tenant handling
const scenario = await emailHelpers.createEmailScenario();
// Act - Send email
await scenario.sendEmail({
subject: 'Test Email',
body: 'Test body'
});
await scenario.waitForProcessing();
// Assert - Verify results
const tickets = await scenario.getTickets();
expect(tickets).toHaveLength(1);
expect(tickets[0].title).toContain('Test Email');
});
});
Core Abstractions
1. Email Test Helpers (EmailTestHelpers)
The main abstraction that handles:
- ✅ Tenant creation and synchronization
- ✅ Database transaction management
- ✅ MailHog service coordination
- ✅ Data visibility across connections
- ✅ Debug logging
Methods:
createEmailScenario()- Create complete test scenariocreateUnknownEmailScenario()- For testing unknown senders
2. Email Test Scenario (EmailTestScenario)
A scenario object that provides:
- ✅ Pre-configured tenant, company, and contact
- ✅ Simple email sending with automatic tenant handling
- ✅ Workflow processing coordination
- ✅ Easy data retrieval methods
Methods:
sendEmail(config)- Send email with tenant synchronizationwaitForProcessing(timeout?)- Wait for workflow completiongetTickets()- Get tickets for this scenario's contactgetComments(ticketId)- Get comments for a ticketgetDocuments()- Get attachments/documents
3. Static Assertion Helpers (EmailTestHelpers)
Pre-built assertions for common test scenarios:
- ✅
assertTicketCreated()- Verify ticket creation - ✅
assertAttachmentProcessed()- Verify attachment handling - ✅
assertEmailThreading()- Verify reply threading
Test Patterns
Pattern 1: Simple Email Processing
it('should process a simple email', async () => {
const scenario = await emailHelpers.createEmailScenario();
await scenario.sendEmail({
subject: 'Support Request',
body: 'Please help me with this issue.'
});
await scenario.waitForProcessing();
const tickets = await scenario.getTickets();
EmailTestHelpers.assertTicketCreated(tickets, 'Support Request', scenario.contact.email);
});
Pattern 2: Email with Attachments
it('should handle email attachments', async () => {
const scenario = await emailHelpers.createEmailScenario();
await scenario.sendEmail({
subject: 'Document Upload',
body: 'Please see attached document.',
attachments: [{
filename: 'report.pdf',
content: Buffer.from('PDF content'),
contentType: 'application/pdf'
}]
});
await scenario.waitForProcessing();
const documents = await scenario.getDocuments();
EmailTestHelpers.assertAttachmentProcessed(documents, 'report.pdf');
});
Pattern 3: Email Threading/Replies
it('should thread email replies', async () => {
const scenario = await emailHelpers.createEmailScenario();
// Send initial email
const { sentEmail } = await scenario.sendEmail({
subject: 'Initial Request',
body: 'Original message'
});
await scenario.waitForProcessing();
const initialTickets = await scenario.getTickets();
// Send reply
await scenario.sendEmail({
subject: 'Re: Initial Request',
body: 'Reply message',
inReplyTo: sentEmail.messageId,
references: sentEmail.messageId
});
await scenario.waitForProcessing();
const finalTickets = await scenario.getTickets();
const comments = await scenario.getComments(initialTickets[0].ticket_id);
EmailTestHelpers.assertEmailThreading(
initialTickets, finalTickets, comments,
'Original message', 'Reply message'
);
});
Pattern 4: Unknown Email Addresses
it('should handle unknown senders', async () => {
const unknownScenario = await emailHelpers.createUnknownEmailScenario();
await unknownScenario.sendEmail({
subject: 'Unknown Sender',
body: 'Email from unknown address'
});
await unknownScenario.waitForProcessing();
const tickets = await unknownScenario.getTickets();
// Verify appropriate handling (may create task for manual review)
expect(tickets.length).toBeGreaterThanOrEqual(0);
});
What's Abstracted Away
Before (Manual Pattern)
it('should process email', async () => {
// 20+ lines of tenant setup
const { tenant, company, contact } = await context.emailTestFactory.createBasicEmailScenario();
console.log(`[TENANT-DEBUG] Test scenario created: tenant=${tenant.tenant}`);
const tenantCheck = await context.db('tenants').where('tenant', tenant.tenant).first();
if (!tenantCheck) {
throw new Error(`Tenant not found`);
}
try {
await context.db.raw('COMMIT');
await context.db.raw('BEGIN');
} catch (error) {
// Handle transaction errors
}
await new Promise(resolve => setTimeout(resolve, 3000));
const testEmail = { /* email config */ };
const { sentEmail, capturedEmail } = await context.sendAndCaptureEmail(testEmail);
await context.waitForWorkflowProcessing(30000);
// 10+ lines of manual queries and assertions
const ticketResult = await context.db.raw(`SELECT...`);
const tickets = ticketResult.rows || ticketResult;
expect(tickets).toHaveLength(1);
// etc...
});
After (Abstracted Pattern)
it('should process email', async () => {
const scenario = await emailHelpers.createEmailScenario();
await scenario.sendEmail({
subject: 'Test Email',
body: 'Test body'
});
await scenario.waitForProcessing();
const tickets = await scenario.getTickets();
EmailTestHelpers.assertTicketCreated(tickets, 'Test Email', scenario.contact.email);
});
Benefits
✅ Reduced Complexity
- 5-10 lines vs 50+ lines per test
- No need to understand tenant synchronization details
- Built-in error handling and logging
✅ Consistency
- All tests use the same proven patterns
- Eliminates copy-paste errors
- Standardized debugging output
✅ Maintainability
- Changes to tenant handling logic in one place
- Easy to add new helper methods
- Clear separation of concerns
✅ Readability
- Tests focus on business logic, not infrastructure
- Self-documenting method names
- Consistent assertion patterns
✅ Reliability
- Proven tenant synchronization patterns
- Automatic timeout management
- Robust error handling
Migration Guide
For Existing Tests
- Replace manual tenant handling with
emailHelpers.createEmailScenario() - Replace manual email sending with
scenario.sendEmail() - Replace manual queries with scenario methods (
getTickets(), etc.) - Replace manual assertions with static helper methods
For New Tests
- Import the helper utilities
- Use the abstracted patterns from the start
- Focus on test business logic, not infrastructure
Extension Points
Adding New Scenario Types
// In EmailTestHelpers class
async createBulkEmailScenario(): Promise<BulkEmailScenario> {
// Implementation for testing bulk email processing
}
Adding New Assertions
// In EmailTestHelpers class
static assertPrioritySet(tickets: any[], expectedPriority: string): void {
expect(tickets[0].priority_name).toBe(expectedPriority);
}
Custom Email Configurations
await scenario.sendEmail({
subject: 'Custom Test',
body: 'Custom body',
from: 'custom@sender.com',
to: 'custom@recipient.com',
attachments: [/* custom attachments */],
inReplyTo: 'custom-reply-id',
references: 'custom-references'
});
File Structure
src/test/e2e/
├── utils/
│ ├── email-test-helpers.ts # Main abstraction
│ ├── persistent-test-context.ts # Persistent harness
│ └── ...
├── email-processing-simplified.test.ts # Example using abstractions
├── email-processing-persistent.test.ts # Direct persistent usage
└── email-processing.test.ts # Original manual pattern
This abstraction approach ensures that all future email tests will be fast, reliable, and maintainable while hiding the complexity of tenant synchronization and workflow coordination.