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
303 lines
8.9 KiB
Markdown
303 lines
8.9 KiB
Markdown
# 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
|
|
```typescript
|
|
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 scenario
|
|
- `createUnknownEmailScenario()` - 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 synchronization
|
|
- `waitForProcessing(timeout?)` - Wait for workflow completion
|
|
- `getTickets()` - Get tickets for this scenario's contact
|
|
- `getComments(ticketId)` - Get comments for a ticket
|
|
- `getDocuments()` - 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
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
|
|
1. Replace manual tenant handling with `emailHelpers.createEmailScenario()`
|
|
2. Replace manual email sending with `scenario.sendEmail()`
|
|
3. Replace manual queries with scenario methods (`getTickets()`, etc.)
|
|
4. Replace manual assertions with static helper methods
|
|
|
|
### For New Tests
|
|
1. Import the helper utilities
|
|
2. Use the abstracted patterns from the start
|
|
3. Focus on test business logic, not infrastructure
|
|
|
|
## Extension Points
|
|
|
|
### Adding New Scenario Types
|
|
```typescript
|
|
// In EmailTestHelpers class
|
|
async createBulkEmailScenario(): Promise<BulkEmailScenario> {
|
|
// Implementation for testing bulk email processing
|
|
}
|
|
```
|
|
|
|
### Adding New Assertions
|
|
```typescript
|
|
// In EmailTestHelpers class
|
|
static assertPrioritySet(tickets: any[], expectedPriority: string): void {
|
|
expect(tickets[0].priority_name).toBe(expectedPriority);
|
|
}
|
|
```
|
|
|
|
### Custom Email Configurations
|
|
```typescript
|
|
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. |