cms.c2sgmbh/tests/unit/security/newsletter-unsubscribe.unit.spec.ts
Martin Porwoll e3987e50dc feat: security hardening, monitoring improvements, and API guards
- Hardened cron endpoints with coordination and auth improvements
- Added API guards and input validation layer
- Security observability and secrets health checks
- Monitoring types and service improvements
- PDF URL validation and newsletter unsubscribe security
- Unit tests for security-critical paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:42:56 +00:00

87 lines
2.7 KiB
TypeScript

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)
})
})