import { describe, it, expect, vi } from 'vitest' import { NewsletterService } from '@/lib/email/newsletter-service' vi.mock('@/lib/email/tenant-email-service', () => ({ sendTenantEmail: vi.fn().mockResolvedValue(undefined), })) interface MockSubscriber { id: number email: string firstName?: string status: 'pending' | 'confirmed' | 'unsubscribed' confirmationToken: string subscribedAt?: string confirmedAt?: string | null unsubscribedAt?: string | null tenant: number } function createMockPayload(subscriber: MockSubscriber) { return { find: vi.fn().mockImplementation(async ({ where }) => { const token = (where as any)?.confirmationToken?.equals if (token && subscriber.confirmationToken === token) { return { docs: [{ ...subscriber }] } } return { docs: [] } }), update: vi.fn().mockImplementation(async ({ data }) => { Object.assign(subscriber, data) return { ...subscriber } }), findByID: vi.fn().mockResolvedValue({ id: subscriber.tenant, name: 'Demo Tenant', domains: [], }), } } describe('Newsletter unsubscribe security', () => { it('rotates token after confirm and again after unsubscribe, blocking replay', async () => { const subscriber: MockSubscriber = { id: 10, email: 'user@example.com', status: 'pending', confirmationToken: 'confirm-token', subscribedAt: new Date().toISOString(), tenant: 1, } const payload = createMockPayload(subscriber) const service = new NewsletterService(payload as any) const confirmResult = await service.confirmSubscription('confirm-token') expect(confirmResult.success).toBe(true) expect(subscriber.status).toBe('confirmed') expect(subscriber.confirmationToken).not.toBe('confirm-token') const unsubscribeToken = subscriber.confirmationToken const unsubscribeResult = await service.unsubscribe(unsubscribeToken) expect(unsubscribeResult.success).toBe(true) expect(subscriber.status).toBe('unsubscribed') expect(subscriber.confirmationToken).not.toBe(unsubscribeToken) const replayResult = await service.unsubscribe(unsubscribeToken) expect(replayResult.success).toBe(false) }) it('does not support predictable ID fallback tokens', async () => { const subscriber: MockSubscriber = { id: 55, email: 'id-test@example.com', status: 'confirmed', confirmationToken: 'random-token', confirmedAt: new Date().toISOString(), tenant: 1, } const payload = createMockPayload(subscriber) const service = new NewsletterService(payload as any) const result = await service.unsubscribe('55') expect(result.success).toBe(false) }) })