diff --git a/src/server/db/campaigns.test.ts b/src/server/db/campaigns.test.ts new file mode 100644 index 0000000..74c475e --- /dev/null +++ b/src/server/db/campaigns.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockClient = vi.hoisted(() => ({ + execute: vi.fn(), + query: vi.fn(), +})) + +vi.mock('./tenant', () => ({ + withTenant: vi.fn((_, fn) => fn(mockClient)), +})) + +import { + createCampaign, + getCampaign, + updateCampaign, + listCampaigns, + updateCampaignStatus, +} from './campaigns' + +const mockRow = { + id: 'uuid-1', + name: 'Test', + subject: 'Betreff', + html_body: '
Hi
', + plain_body: 'Hi', + status: 'draft', + scheduled_at: null, + cron_expression: null, + created_at: new Date('2026-01-01'), + updated_at: new Date('2026-01-01'), +} + +describe('createCampaign', () => { + beforeEach(() => vi.clearAllMocks()) + + it('gibt erstellte Kampagne zurück', async () => { + mockClient.query.mockResolvedValueOnce([mockRow]) + const result = await createCampaign('tenant1', { + name: 'Test', + subject: 'Betreff', + htmlBody: 'Hi
', + plainBody: 'Hi', + }) + expect(result.ok).toBe(true) + if (result.ok) expect(result.data.name).toBe('Test') + }) + + it('mapped html_body → htmlBody korrekt', async () => { + mockClient.query.mockResolvedValueOnce([mockRow]) + const result = await createCampaign('tenant1', { + name: 'Test', subject: 'Betreff', htmlBody: 'Hi
', plainBody: 'Hi', + }) + if (result.ok) expect(result.data.htmlBody).toBe('Hi
') + }) +}) + +describe('getCampaign', () => { + beforeEach(() => vi.clearAllMocks()) + + it('gibt Kampagne zurück wenn gefunden', async () => { + mockClient.query.mockResolvedValueOnce([mockRow]) + const result = await getCampaign('tenant1', 'uuid-1') + expect(result.ok).toBe(true) + }) + + it('gibt err zurück wenn nicht gefunden', async () => { + mockClient.query.mockResolvedValueOnce([]) + const result = await getCampaign('tenant1', 'uuid-1') + expect(result.ok).toBe(false) + if (!result.ok) expect(result.error.message).toContain('nicht gefunden') + }) +}) + +describe('listCampaigns', () => { + beforeEach(() => vi.clearAllMocks()) + + it('gibt leeres Array zurück wenn keine Kampagnen', async () => { + mockClient.query.mockResolvedValueOnce([]) + const result = await listCampaigns('tenant1') + expect(result.ok).toBe(true) + if (result.ok) expect(result.data).toEqual([]) + }) + + it('gibt mehrere Kampagnen zurück', async () => { + mockClient.query.mockResolvedValueOnce([mockRow, { ...mockRow, id: 'uuid-2' }]) + const result = await listCampaigns('tenant1') + if (result.ok) expect(result.data).toHaveLength(2) + }) +}) + +describe('updateCampaign', () => { + beforeEach(() => vi.clearAllMocks()) + + it('aktualisiert nur übergebene Felder', async () => { + mockClient.query.mockResolvedValueOnce([{ ...mockRow, name: 'Neu' }]) + const result = await updateCampaign('tenant1', 'uuid-1', { name: 'Neu' }) + expect(result.ok).toBe(true) + if (result.ok) expect(result.data.name).toBe('Neu') + }) + + it('gibt err zurück wenn keine Felder übergeben', async () => { + const result = await updateCampaign('tenant1', 'uuid-1', {}) + expect(result.ok).toBe(false) + }) +}) + +describe('updateCampaignStatus', () => { + beforeEach(() => vi.clearAllMocks()) + + it('setzt Status auf scheduled', async () => { + mockClient.query.mockResolvedValueOnce([{ ...mockRow, status: 'scheduled' }]) + const result = await updateCampaignStatus('tenant1', 'uuid-1', 'scheduled') + expect(result.ok).toBe(true) + if (result.ok) expect(result.data.status).toBe('scheduled') + }) + + it('gibt err zurück wenn Kampagne nicht gefunden', async () => { + mockClient.query.mockResolvedValueOnce([]) + const result = await updateCampaignStatus('tenant1', 'uuid-1', 'sent') + expect(result.ok).toBe(false) + }) +}) diff --git a/src/server/db/campaigns.ts b/src/server/db/campaigns.ts new file mode 100644 index 0000000..67bdfac --- /dev/null +++ b/src/server/db/campaigns.ts @@ -0,0 +1,115 @@ +import { withTenant } from './tenant' +import { ok, err, type Result } from '../../lib/result' +import type { Campaign, CampaignStatus } from '../../types' +import type { CreateCampaignInput, UpdateCampaignInput } from '../../lib/validation' + +function rowToCampaign(row: Record