Files
plunk/test/helpers/jobs.ts
T

195 lines
4.7 KiB
TypeScript

import { vi } from 'vitest';
import { Queue, Worker, Job } from 'bullmq';
/**
* Job testing helper
* Provides utilities to test BullMQ job processors without Redis
*/
export interface MockJob<T = any> {
id: string;
data: T;
attemptsMade: number;
processedOn?: number;
finishedOn?: number;
returnvalue?: any;
failedReason?: string;
}
/**
* Create a mock BullMQ job
*/
export function createMockJob<T>(data: T, options: Partial<MockJob<T>> = {}): Job<T> {
const mockJob = {
id: options.id || `job-${Date.now()}`,
data,
attemptsMade: options.attemptsMade || 0,
processedOn: options.processedOn,
finishedOn: options.finishedOn,
returnvalue: options.returnvalue,
failedReason: options.failedReason,
// Mock methods
updateProgress: vi.fn(),
log: vi.fn(),
moveToCompleted: vi.fn(),
moveToFailed: vi.fn(),
remove: vi.fn(),
retry: vi.fn(),
} as unknown as Job<T>;
return mockJob;
}
/**
* Mock queue for testing job creation
*/
export class MockQueue<T = any> {
private jobs: Array<{ name?: string; data: T; opts?: any }> = [];
async add(name: string | T, data?: T | any, opts?: any) {
// Handle both queue.add(data) and queue.add(name, data) patterns
const jobName = typeof name === 'string' ? name : undefined;
const jobData = typeof name === 'string' ? data : name;
const jobOpts = typeof name === 'string' ? opts : data;
this.jobs.push({
name: jobName,
data: jobData,
opts: jobOpts,
});
return createMockJob(jobData, { id: `mock-${this.jobs.length}` });
}
getJobs() {
return this.jobs;
}
getJobsByName(name: string) {
return this.jobs.filter((job) => job.name === name);
}
clear() {
this.jobs = [];
}
async close() {
// No-op for mock
}
}
/**
* Create mock queues for testing
*/
export function createMockQueues() {
return {
email: new MockQueue(),
campaign: new MockQueue(),
scheduledCampaign: new MockQueue(),
workflow: new MockQueue(),
import: new MockQueue(),
segmentCount: new MockQueue(),
domainVerification: new MockQueue(),
};
}
/**
* Job processor test helper
* Allows testing job processors in isolation
*/
export class JobProcessorTester<T = any> {
private processor: (job: Job<T>) => Promise<any>;
constructor(processor: (job: Job<T>) => Promise<any>) {
this.processor = processor;
}
/**
* Process a job with test data
*/
async process(data: T, options: Partial<MockJob<T>> = {}) {
const job = createMockJob(data, options);
return this.processor(job);
}
/**
* Process multiple jobs
*/
async processMany(dataArray: T[], options: Partial<MockJob<T>> = {}) {
const results = [];
for (const data of dataArray) {
const result = await this.process(data, options);
results.push(result);
}
return results;
}
}
/**
* Create a job processor tester
*/
export function createJobTester<T = any>(processor: (job: Job<T>) => Promise<any>) {
return new JobProcessorTester<T>(processor);
}
/**
* Mock external services for job testing
*/
export function createServiceMocks() {
return {
// Mock SES (email sending)
ses: {
sendEmail: vi.fn().mockResolvedValue({ MessageId: 'mock-message-id' }),
sendRawEmail: vi.fn().mockResolvedValue({ MessageId: 'mock-message-id' }),
},
// Mock S3 (file storage)
s3: {
upload: vi.fn().mockResolvedValue({ Location: 'https://mock-s3.com/file.jpg' }),
getSignedUrl: vi.fn().mockResolvedValue('https://mock-s3.com/signed-url'),
deleteObject: vi.fn().mockResolvedValue({}),
},
// Mock Stripe (billing)
stripe: {
customers: {
create: vi.fn().mockResolvedValue({ id: 'cus_mock' }),
retrieve: vi.fn().mockResolvedValue({ id: 'cus_mock' }),
},
subscriptions: {
create: vi.fn().mockResolvedValue({ id: 'sub_mock' }),
update: vi.fn().mockResolvedValue({ id: 'sub_mock' }),
},
},
// Mock Redis
redis: {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue('OK'),
del: vi.fn().mockResolvedValue(1),
incr: vi.fn().mockResolvedValue(1),
expire: vi.fn().mockResolvedValue(1),
},
};
}
/**
* Wait for jobs to complete (useful in integration tests with real queues)
*/
export async function waitForJobs(queue: Queue, timeout = 5000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const counts = await queue.getJobCounts('waiting', 'active', 'delayed');
const pending = counts.waiting + counts.active + counts.delayed;
if (pending === 0) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Jobs did not complete within ${timeout}ms`);
}