import { vi } from 'vitest'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; import type { Queue, Job } from 'bullmq'; /** * In-memory queue implementation for testing * Provides realistic queue behavior without Redis */ export class MockQueueStore { private jobs = new Map>(); private jobCounter = 0; async add(name: string | T, data?: T | any, opts?: any): Promise> { const jobName = typeof name === 'string' ? name : undefined; const jobData = typeof name === 'string' ? data : name; const jobOpts = typeof name === 'string' ? opts : data; const jobId = jobOpts?.jobId || `job-${++this.jobCounter}`; const job: MockJobData = { id: jobId, name: jobName, data: jobData, opts: jobOpts, state: 'waiting', attemptsMade: 0, timestamp: Date.now(), processedOn: undefined, finishedOn: undefined, returnvalue: undefined, failedReason: undefined, }; this.jobs.set(jobId, job); return job; } async getJob(jobId: string): Promise | null> { return this.jobs.get(jobId) || null; } async getJobs(states: string[]): Promise[]> { return Array.from(this.jobs.values()).filter((job) => states.includes(job.state)); } async getJobCounts(...states: string[]): Promise> { const counts: Record = {}; for (const state of states) { counts[state] = Array.from(this.jobs.values()).filter((j) => j.state === state).length; } return counts; } async removeJob(jobId: string): Promise { this.jobs.delete(jobId); } async pause(): Promise { // No-op for mock } async resume(): Promise { // No-op for mock } async clean(grace: number, limit: number, type: string): Promise { const now = Date.now(); const removed: string[] = []; for (const [jobId, job] of this.jobs.entries()) { if (job.state === type && job.finishedOn && now - job.finishedOn > grace) { this.jobs.delete(jobId); removed.push(jobId); if (removed.length >= limit) break; } } return removed; } async close(): Promise { this.jobs.clear(); } // Test utilities getAllJobs(): MockJobData[] { return Array.from(this.jobs.values()); } getJobsByName(name: string): MockJobData[] { return Array.from(this.jobs.values()).filter((job) => job.name === name); } clear(): void { this.jobs.clear(); this.jobCounter = 0; } size(): number { return this.jobs.size; } markJobAsCompleted(jobId: string, returnvalue?: any): void { const job = this.jobs.get(jobId); if (job) { job.state = 'completed'; job.finishedOn = Date.now(); job.returnvalue = returnvalue; } } markJobAsFailed(jobId: string, error: string): void { const job = this.jobs.get(jobId); if (job) { job.state = 'failed'; job.finishedOn = Date.now(); job.failedReason = error; } } } export interface MockJobData { id: string; name?: string; data: T; opts?: any; state: 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'; attemptsMade: number; timestamp: number; processedOn?: number; finishedOn?: number; returnvalue?: any; failedReason?: string; } /** * Create mock BullMQ queues for testing */ export function createMockQueues() { return { email: new MockQueueStore(), campaign: new MockQueueStore(), workflow: new MockQueueStore(), scheduled: new MockQueueStore(), import: new MockQueueStore(), segmentCount: new MockQueueStore(), domainVerification: new MockQueueStore(), }; } /** * Create a typed mock queue using vitest-mock-extended */ export function createMockQueue(): DeepMockProxy> { const mock = mockDeep>(); // Default implementations mock.add.mockImplementation(async (name, data, opts) => { return { id: opts?.jobId || `mock-job-${Date.now()}`, data: (typeof name === 'string' ? data : name) as T, attemptsMade: 0, } as unknown as Job; }); mock.getJob.mockResolvedValue(null); mock.getJobs.mockResolvedValue([]); mock.getJobCounts.mockResolvedValue({ waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, }); mock.pause.mockResolvedValue(undefined); mock.resume.mockResolvedValue(undefined); mock.close.mockResolvedValue(undefined); return mock; }