mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 15:04:14 +00:00
- 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>
87 lines
2.7 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
|