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 { createSubscriber, confirmDoi, deleteSubscriber } from './subscribers' const mockRow = { id: 'sub-uuid-1', email: 'test@example.com', email_hash: 'abc123hash', status: 'pending', list_id: null, doi_token: 'token-uuid', doi_requested_at: new Date('2026-04-17').toISOString(), doi_confirmed_at: null, consent_ip: '1.2.3.4', consent_user_agent: 'Mozilla/5.0', created_at: new Date('2026-04-17').toISOString(), updated_at: new Date('2026-04-17').toISOString(), } describe('createSubscriber', () => { beforeEach(() => mockClient.query.mockReset()) it('legt Subscriber im status=pending an', async () => { mockClient.query.mockResolvedValueOnce([mockRow]) const result = await createSubscriber('tenant1', { email: 'test@example.com', consentIp: '1.2.3.4', consentUserAgent: 'Mozilla/5.0', }) expect(result.ok).toBe(true) if (result.ok) expect(result.data.status).toBe('pending') }) it('speichert email_hash statt Klartext in Query-Params', async () => { mockClient.query.mockResolvedValueOnce([mockRow]) await createSubscriber('tenant1', { email: 'Test@Example.com', consentIp: '1.2.3.4', consentUserAgent: 'UA', }) const params = mockClient.query.mock.calls[0][1] as unknown[] const hashParam = params.find((p) => typeof p === 'string' && /^[a-f0-9]{64}$/.test(p)) expect(hashParam).toBeDefined() }) it('sendet Klartext-E-Mail nie als einzigen Parameter', async () => { mockClient.query.mockResolvedValueOnce([mockRow]) await createSubscriber('tenant1', { email: 'secret@example.com', consentIp: '1.2.3.4', consentUserAgent: 'UA', }) const params = mockClient.query.mock.calls[0][1] as unknown[] // E-Mail darf vorkommen (für SMTP-Zwecke im subscribers-Record), // aber der Hash muss ebenfalls vorhanden sein const hasHash = params.some((p) => typeof p === 'string' && /^[a-f0-9]{64}$/.test(p)) expect(hasHash).toBe(true) }) it('speichert consentIp und consentUserAgent', async () => { mockClient.query.mockResolvedValueOnce([mockRow]) await createSubscriber('tenant1', { email: 'test@example.com', consentIp: '9.9.9.9', consentUserAgent: 'TestAgent', }) const params = mockClient.query.mock.calls[0][1] as unknown[] expect(params).toContain('9.9.9.9') expect(params).toContain('TestAgent') }) it('gibt err zurück wenn E-Mail bereits registriert (ON CONFLICT)', async () => { mockClient.query.mockResolvedValueOnce([]) const result = await createSubscriber('tenant1', { email: 'exists@example.com', consentIp: '1.2.3.4', consentUserAgent: 'UA', }) expect(result.ok).toBe(false) if (!result.ok) expect(result.error.message).toContain('bereits registriert') }) }) describe('confirmDoi', () => { beforeEach(() => mockClient.query.mockReset()) it('setzt status=active bei gültigem Token', async () => { mockClient.query.mockResolvedValueOnce([{ ...mockRow, status: 'active', doi_token: null, doi_confirmed_at: new Date().toISOString() }]) const result = await confirmDoi('tenant1', 'valid-token') expect(result.ok).toBe(true) if (result.ok) expect(result.data.status).toBe('active') }) it('gibt err zurück bei ungültigem Token', async () => { mockClient.query.mockResolvedValueOnce([]) const result = await confirmDoi('tenant1', 'wrong-token') expect(result.ok).toBe(false) if (!result.ok) expect(result.error.message).toContain('Ungültiger') }) it('löscht doi_token nach Bestätigung', async () => { mockClient.query.mockResolvedValueOnce([{ ...mockRow, status: 'active', doi_token: null }]) const result = await confirmDoi('tenant1', 'valid-token') if (result.ok) expect(result.data.doiToken).toBeNull() }) }) describe('deleteSubscriber', () => { beforeEach(() => mockClient.query.mockReset()) it('löscht Subscriber und gibt ok zurück', async () => { mockClient.query.mockResolvedValueOnce([{ id: 'sub-uuid-1' }]) const result = await deleteSubscriber('tenant1', 'sub-uuid-1') expect(result.ok).toBe(true) }) it('gibt err zurück wenn Subscriber nicht existiert', async () => { mockClient.query.mockResolvedValueOnce([]) const result = await deleteSubscriber('tenant1', 'not-found') expect(result.ok).toBe(false) if (!result.ok) expect(result.error.message).toContain('nicht gefunden') }) })