fix: DI statt Singleton, Queue-Name email-send (BullMQ v5 kein Doppelpunkt), err bei fehlender Job-ID
This commit is contained in:
@@ -3,19 +3,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|||||||
const mockAdd = vi.hoisted(() => vi.fn().mockResolvedValue({ id: 'job-1' }))
|
const mockAdd = vi.hoisted(() => vi.fn().mockResolvedValue({ id: 'job-1' }))
|
||||||
|
|
||||||
vi.mock('bullmq', () => ({
|
vi.mock('bullmq', () => ({
|
||||||
Queue: vi.fn().mockImplementation(() => ({
|
Queue: vi.fn().mockImplementation(() => ({ add: mockAdd })),
|
||||||
add: mockAdd,
|
|
||||||
})),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { enqueueEmailSend, type EmailSendJobData, resetQueueForTesting } from './email-send.queue'
|
import { enqueueEmailSend, emailSendQueue, type EmailSendJobData } from './email-send.queue'
|
||||||
import { Queue } from 'bullmq'
|
|
||||||
|
|
||||||
describe('enqueueEmailSend', () => {
|
describe('enqueueEmailSend', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => vi.clearAllMocks())
|
||||||
vi.clearAllMocks()
|
|
||||||
resetQueueForTesting()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('enqueued Job mit korrekten Daten', async () => {
|
it('enqueued Job mit korrekten Daten', async () => {
|
||||||
const data: EmailSendJobData = {
|
const data: EmailSendJobData = {
|
||||||
@@ -24,7 +18,7 @@ describe('enqueueEmailSend', () => {
|
|||||||
recipientEmail: 'empfaenger@example.com',
|
recipientEmail: 'empfaenger@example.com',
|
||||||
recipientHash: 'abc123hash',
|
recipientHash: 'abc123hash',
|
||||||
}
|
}
|
||||||
const result = await enqueueEmailSend(data)
|
const result = await enqueueEmailSend(data, emailSendQueue)
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
expect(mockAdd).toHaveBeenCalledWith('send', data, expect.objectContaining({ attempts: 3 }))
|
expect(mockAdd).toHaveBeenCalledWith('send', data, expect.objectContaining({ attempts: 3 }))
|
||||||
})
|
})
|
||||||
@@ -36,7 +30,8 @@ describe('enqueueEmailSend', () => {
|
|||||||
recipientEmail: 'x@example.com',
|
recipientEmail: 'x@example.com',
|
||||||
recipientHash: 'hash1',
|
recipientHash: 'hash1',
|
||||||
}
|
}
|
||||||
const result = await enqueueEmailSend(data)
|
const result = await enqueueEmailSend(data, emailSendQueue)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
if (result.ok) expect(result.data).toBe('job-1')
|
if (result.ok) expect(result.data).toBe('job-1')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -44,7 +39,7 @@ describe('enqueueEmailSend', () => {
|
|||||||
mockAdd.mockRejectedValueOnce(new Error('Redis down'))
|
mockAdd.mockRejectedValueOnce(new Error('Redis down'))
|
||||||
const result = await enqueueEmailSend({
|
const result = await enqueueEmailSend({
|
||||||
tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1',
|
tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1',
|
||||||
})
|
}, emailSendQueue)
|
||||||
expect(result.ok).toBe(false)
|
expect(result.ok).toBe(false)
|
||||||
if (!result.ok) expect(result.error.message).toBe('Redis down')
|
if (!result.ok) expect(result.error.message).toBe('Redis down')
|
||||||
})
|
})
|
||||||
@@ -52,19 +47,20 @@ describe('enqueueEmailSend', () => {
|
|||||||
it('nutzt exponentielles Backoff', async () => {
|
it('nutzt exponentielles Backoff', async () => {
|
||||||
await enqueueEmailSend({
|
await enqueueEmailSend({
|
||||||
tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1',
|
tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1',
|
||||||
})
|
}, emailSendQueue)
|
||||||
expect(mockAdd).toHaveBeenCalledWith(
|
expect(mockAdd).toHaveBeenCalledWith(
|
||||||
'send',
|
'send',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({ backoff: expect.objectContaining({ type: 'exponential' }) })
|
||||||
backoff: expect.objectContaining({ type: 'exponential' }),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Queue wird mit Queue-Name "email:send" initialisiert', async () => {
|
it('gibt err zurück wenn Job ohne ID erstellt wird', async () => {
|
||||||
await enqueueEmailSend({ tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1' })
|
mockAdd.mockResolvedValueOnce({ id: undefined })
|
||||||
const { Queue: MockQueue } = await import('bullmq')
|
const result = await enqueueEmailSend({
|
||||||
expect(MockQueue).toHaveBeenCalledWith('email:send', expect.anything())
|
tenantId: 't1', campaignId: 'c1', recipientEmail: 'x@x.com', recipientHash: 'h1',
|
||||||
|
}, emailSendQueue)
|
||||||
|
expect(result.ok).toBe(false)
|
||||||
|
if (!result.ok) expect(result.error.message).toContain('ohne ID')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,29 +13,21 @@ const connection = {
|
|||||||
port: Number(process.env.REDIS_PORT ?? 6379),
|
port: Number(process.env.REDIS_PORT ?? 6379),
|
||||||
}
|
}
|
||||||
|
|
||||||
let _queue: Queue<EmailSendJobData> | null = null
|
export const emailSendQueue = new Queue<EmailSendJobData>('email-send', { connection })
|
||||||
|
|
||||||
function getQueue(): Queue<EmailSendJobData> {
|
export async function enqueueEmailSend(
|
||||||
if (!_queue) {
|
data: EmailSendJobData,
|
||||||
_queue = new Queue<EmailSendJobData>('email:send', { connection })
|
queue: Queue<EmailSendJobData> = emailSendQueue
|
||||||
}
|
): Promise<Result<string>> {
|
||||||
return _queue
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Nur für Tests — setzt den Queue-Singleton zurück */
|
|
||||||
export function resetQueueForTesting(): void {
|
|
||||||
_queue = null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function enqueueEmailSend(data: EmailSendJobData): Promise<Result<string>> {
|
|
||||||
try {
|
try {
|
||||||
const job = await getQueue().add('send', data, {
|
const job = await queue.add('send', data, {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
backoff: { type: 'exponential', delay: 2000 },
|
backoff: { type: 'exponential', delay: 2000 },
|
||||||
removeOnComplete: 100,
|
removeOnComplete: 100,
|
||||||
removeOnFail: { count: 500 },
|
removeOnFail: { count: 500 },
|
||||||
})
|
})
|
||||||
return ok(job.id ?? 'unknown')
|
if (!job.id) return err(new Error('Job wurde ohne ID erstellt'))
|
||||||
|
return ok(job.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return err(e instanceof Error ? e : new Error(String(e)))
|
return err(e instanceof Error ? e : new Error(String(e)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user