diff --git a/src/queues/email-send.worker.test.ts b/src/queues/email-send.worker.test.ts new file mode 100644 index 0000000..c599936 --- /dev/null +++ b/src/queues/email-send.worker.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('../server/db/campaigns', () => ({ + getCampaign: vi.fn(), +})) +vi.mock('../server/suppression/check', () => ({ + checkSuppression: vi.fn(), +})) +vi.mock('../server/smtp/client', () => ({ + sendEmail: vi.fn(), +})) +vi.mock('../server/clickhouse/client', () => ({ + clickhouse: { insert: vi.fn().mockResolvedValue(undefined) }, +})) + +import { processEmailSendJob } from './email-send.worker' +import { getCampaign } from '../server/db/campaigns' +import { checkSuppression } from '../server/suppression/check' +import { sendEmail } from '../server/smtp/client' +import { clickhouse } from '../server/clickhouse/client' + +const mockCampaign = { + id: 'campaign-1', + subject: 'Newsletter April', + htmlBody: '
Hallo
', + plainBody: 'Hallo', + name: 'April Newsletter', + status: 'sending' as const, + scheduledAt: null, + cronExpression: null, + createdAt: new Date(), + updatedAt: new Date(), +} + +const jobData = { + tenantId: 'tenant1', + campaignId: 'campaign-1', + recipientEmail: 'empfaenger@example.com', + recipientHash: 'abc123hash', +} + +describe('processEmailSendJob', () => { + beforeEach(() => vi.clearAllMocks()) + + it('sendet E-Mail wenn nicht suppressed', async () => { + vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign }) + vi.mocked(checkSuppression).mockResolvedValue(false) + vi.mocked(sendEmail).mockResolvedValue({ ok: true, data: undefined }) + + const result = await processEmailSendJob(jobData) + + expect(result.ok).toBe(true) + expect(sendEmail).toHaveBeenCalledOnce() + expect(clickhouse.insert).toHaveBeenCalledOnce() + }) + + it('überspringt SMTP wenn Empfänger suppressed ist', async () => { + vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign }) + vi.mocked(checkSuppression).mockResolvedValue(true) + + const result = await processEmailSendJob(jobData) + + expect(result.ok).toBe(true) + expect(sendEmail).not.toHaveBeenCalled() + expect(clickhouse.insert).toHaveBeenCalledWith( + expect.objectContaining({ + values: expect.arrayContaining([ + expect.objectContaining({ event_type: 'suppressed' }), + ]), + }) + ) + }) + + it('gibt err zurück wenn Kampagne nicht gefunden', async () => { + vi.mocked(getCampaign).mockResolvedValue({ ok: false, error: new Error('nicht gefunden') }) + + const result = await processEmailSendJob(jobData) + + expect(result.ok).toBe(false) + if (!result.ok) expect(result.error.message).toContain('nicht gefunden') + }) + + it('gibt err zurück wenn SMTP fehlschlägt', async () => { + vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign }) + vi.mocked(checkSuppression).mockResolvedValue(false) + vi.mocked(sendEmail).mockResolvedValue({ ok: false, error: new Error('SMTP-Fehler') }) + + const result = await processEmailSendJob(jobData) + + expect(result.ok).toBe(false) + if (!result.ok) expect(result.error.message).toBe('SMTP-Fehler') + }) + + it('ClickHouse-Event enthält recipient_hash (kein Klartext)', async () => { + vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign }) + vi.mocked(checkSuppression).mockResolvedValue(false) + vi.mocked(sendEmail).mockResolvedValue({ ok: true, data: undefined }) + + await processEmailSendJob(jobData) + + const insertCall = vi.mocked(clickhouse.insert).mock.calls[0][0] + const eventValue = (insertCall.values as Array