/** * Security API Integration Tests * * Tests for security enforcement on API endpoints. * Covers rate limiting, CSRF protection, and IP blocking. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { NextRequest, NextResponse } from 'next/server' // Enable CSRF validation in CI by setting BYPASS_CSRF=false vi.stubEnv('BYPASS_CSRF', 'false') import { generateTestCsrfToken, generateExpiredCsrfToken, generateInvalidCsrfToken, createMockRequest, randomIp, uniqueTestId, } from '../helpers/security-test-utils' // ============================================================================ // Mock Setup // ============================================================================ // Mock Redis (not available in test environment) vi.mock('ioredis', () => ({ default: vi.fn().mockImplementation(() => ({ incr: vi.fn().mockResolvedValue(1), pexpire: vi.fn().mockResolvedValue(1), pttl: vi.fn().mockResolvedValue(60000), keys: vi.fn().mockResolvedValue([]), del: vi.fn().mockResolvedValue(0), quit: vi.fn(), on: vi.fn(), })), })) vi.mock('@/lib/redis', () => ({ getRedisClient: vi.fn(() => null), isRedisAvailable: vi.fn(() => false), })) // Mock Payload for endpoints that need it vi.mock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: null }), find: vi.fn().mockResolvedValue({ docs: [], totalDocs: 0, page: 1, totalPages: 0 }), create: vi.fn().mockResolvedValue({ id: 1 }), login: vi.fn().mockRejectedValue(new Error('Invalid credentials')), }), })) // Mock config vi.mock('@payload-config', () => ({ default: Promise.resolve({}), })) describe('Security API Integration', () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() // Set up environment vi.stubEnv('CSRF_SECRET', 'test-csrf-secret-for-testing') vi.stubEnv('PAYLOAD_PUBLIC_SERVER_URL', 'https://test.example.com') vi.stubEnv('NEXT_PUBLIC_SERVER_URL', 'https://test.example.com') }) afterEach(() => { vi.useRealTimers() vi.unstubAllEnvs() }) describe('Rate Limiting', () => { describe('searchLimiter on /api/posts', () => { it('includes rate limit headers on success', async () => { // Reset modules to get fresh rate limiter state vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const testIp = uniqueTestId('ip') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: testIp, }) const response = await GET(req) expect(response.headers.get('X-RateLimit-Limit')).toBe('30') expect(response.headers.get('X-RateLimit-Remaining')).toBeDefined() expect(response.headers.get('X-RateLimit-Reset')).toBeDefined() }) it('returns 429 when rate limit exceeded', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const testIp = uniqueTestId('blocked-ip') const req = () => createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: testIp, }) // Exhaust rate limit (30 requests) for (let i = 0; i < 30; i++) { await GET(req()) } // 31st request should be blocked const response = await GET(req()) expect(response.status).toBe(429) expect(response.headers.get('Retry-After')).toBeDefined() }) }) describe('searchLimiter on /api/search', () => { it('includes rate limit headers', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/search/route') const testIp = uniqueTestId('search-ip') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/search?q=test', ip: testIp, }) const response = await GET(req) expect(response.headers.has('X-RateLimit-Remaining')).toBe(true) }) }) }) describe('IP Blocking', () => { it('blocks requests from blocked IPs on /api/posts', async () => { vi.stubEnv('BLOCKED_IPS', '192.168.100.50') vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: '192.168.100.50', }) const response = await GET(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error).toContain('Access denied') }) it('blocks requests from blocked CIDR range', async () => { vi.stubEnv('BLOCKED_IPS', '10.0.0.0/8') vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: '10.50.100.200', }) const response = await GET(req) expect(response.status).toBe(403) }) it('allows requests from non-blocked IPs', async () => { vi.stubEnv('BLOCKED_IPS', '192.168.100.50') vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: '8.8.8.8', }) const response = await GET(req) expect(response.status).not.toBe(403) }) }) describe('CSRF Protection', () => { describe('Login Endpoints', () => { it('validates CSRF on /api/auth/login', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') // Request without CSRF token const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', }, ip: randomIp(), }) // Mock json() method Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error || body.errors?.[0]?.message).toContain('CSRF') }) it('allows login with valid CSRF token', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) // Mock json() method Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) // Should proceed past CSRF check (may fail auth, but that's expected) expect(response.status).not.toBe(403) }) it('allows server-to-server login requests without CSRF', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') // Server-to-server: Authorization header, JSON content-type, no Origin const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', authorization: 'Bearer api-token', // No origin header }, ip: randomIp(), }) // Mock json() method Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) // Should not be CSRF blocked expect(response.status).not.toBe(403) }) }) describe('Send Email Endpoint', () => { it('requires CSRF for browser requests', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/send-email/route') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/send-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', }, ip: randomIp(), }) // Mock json() method Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, to: 'recipient@example.com', subject: 'Test', html: '
Test
', }), }) const response = await POST(req) expect(response.status).toBe(403) }) }) describe('Token Validation', () => { it('rejects expired CSRF tokens', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') const expiredToken = generateExpiredCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': expiredToken, }, cookies: { 'csrf-token': expiredToken, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) expect(response.status).toBe(403) }) it('rejects invalid CSRF tokens', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') const invalidToken = generateInvalidCsrfToken() const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': invalidToken, }, cookies: { 'csrf-token': invalidToken, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) expect(response.status).toBe(403) }) it('rejects mismatched header and cookie tokens', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') const token1 = generateTestCsrfToken('test-csrf-secret-for-testing') const token2 = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token1, }, cookies: { 'csrf-token': token2, // Different token }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) expect(response.status).toBe(403) }) }) }) describe('CSRF Token Endpoint', () => { it('returns new CSRF token', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/csrf-token/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/csrf-token', ip: randomIp(), }) const response = await GET(req) expect(response.status).toBe(200) const body = await response.json() expect(body.success).toBe(true) expect(body.token).toBeDefined() expect(body.header).toBe('X-CSRF-Token') expect(body.cookie).toBe('csrf-token') }) it('sets CSRF cookie in response', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/csrf-token/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/csrf-token', ip: randomIp(), }) const response = await GET(req) // Check for Set-Cookie header const setCookie = response.headers.get('set-cookie') expect(setCookie).toContain('csrf-token') }) it('includes rate limit headers', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/csrf-token/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/csrf-token', ip: randomIp(), }) const response = await GET(req) expect(response.headers.has('X-RateLimit-Remaining')).toBe(true) }) }) describe('Safe Methods', () => { it('allows GET requests without CSRF token', async () => { vi.resetModules() const { GET } = await import('@/app/(frontend)/api/posts/route') const req = createMockRequest({ method: 'GET', url: 'https://test.example.com/api/posts', ip: randomIp(), }) const response = await GET(req) // GET should work without CSRF expect(response.status).not.toBe(403) }) }) describe('Origin Validation', () => { it('rejects requests from unauthorized origins', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/auth/login/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/auth/login', headers: { 'content-type': 'application/json', origin: 'https://malicious-site.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ email: 'test@example.com', password: 'password123', }), }) const response = await POST(req) expect(response.status).toBe(403) }) }) // ============================================================================ // Test Email Endpoint Security Tests // ============================================================================ describe('Test Email Endpoint (/api/test-email)', () => { describe('CSRF Protection', () => { it('requires CSRF token for browser requests', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/test-email/route') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error).toContain('CSRF') }) it('accepts valid CSRF token', async () => { vi.resetModules() // Mock authenticated user vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, email: 'admin@test.com', isSuperAdmin: true, tenants: [], }, }), findByID: vi.fn().mockResolvedValue({ id: 1, name: 'Test Tenant' }), create: vi.fn().mockResolvedValue({ id: 1 }), update: vi.fn().mockResolvedValue({}), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) // Should pass CSRF check (may fail on email send, but not 403) expect(response.status).not.toBe(403) }) it('rejects expired CSRF tokens', async () => { vi.resetModules() const { POST } = await import('@/app/(payload)/api/test-email/route') const expiredToken = generateExpiredCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': expiredToken, }, cookies: { 'csrf-token': expiredToken, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(403) }) }) describe('IP Blocking', () => { it('blocks requests from blocked IPs', async () => { vi.stubEnv('BLOCKED_IPS', '192.168.200.100') vi.resetModules() const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: '192.168.200.100', }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error).toContain('Access denied') }) it('respects SEND_EMAIL_ALLOWED_IPS allowlist', async () => { vi.stubEnv('SEND_EMAIL_ALLOWED_IPS', '10.0.0.1') vi.resetModules() const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') // Request from IP not in allowlist const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: '192.168.1.100', // Not in allowlist }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error).toContain('Access denied') }) }) describe('Authentication', () => { it('requires authentication', async () => { vi.resetModules() // Mock unauthenticated user vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: null }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(401) const body = await response.json() expect(body.error).toContain('authentifiziert') }) }) describe('Tenant Access Control', () => { it('allows super admin access to any tenant', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, email: 'superadmin@test.com', isSuperAdmin: true, tenants: [], }, }), findByID: vi.fn().mockResolvedValue({ id: 99, name: 'Any Tenant' }), }), })) // Mock sendTestEmail vi.doMock('@/lib/email/tenant-email-service', () => ({ sendTestEmail: vi.fn().mockResolvedValue({ success: true, messageId: 'test-123', logId: 1, }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 99, recipientEmail: 'test@example.com', }), }) const response = await POST(req) // Super admin should have access expect(response.status).not.toBe(403) }) it('denies access to tenants user is not assigned to', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 2, email: 'user@test.com', isSuperAdmin: false, tenants: [{ tenant: { id: 1 } }], // Only has access to tenant 1 }, }), findByID: vi.fn().mockResolvedValue({ id: 99, name: 'Other Tenant' }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 99, // Different tenant recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(403) const body = await response.json() expect(body.error).toContain('Zugriff') }) it('normalizes tenant object format { tenant: { id } }', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 2, email: 'user@test.com', isSuperAdmin: false, // Multi-tenant plugin format: { tenant: { id: number } } tenants: [{ tenant: { id: 5 } }], }, }), findByID: vi.fn().mockResolvedValue({ id: 5, name: 'User Tenant' }), }), })) vi.doMock('@/lib/email/tenant-email-service', () => ({ sendTestEmail: vi.fn().mockResolvedValue({ success: true, messageId: 'test-456', logId: 2, }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 5, recipientEmail: 'test@example.com', }), }) const response = await POST(req) // Should have access (tenant ID 5 matches) expect(response.status).not.toBe(403) }) it('normalizes tenant primitive format { tenant: number }', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 2, email: 'user@test.com', isSuperAdmin: false, // Alternative format: { tenant: number } tenants: [{ tenant: 7 }], }, }), findByID: vi.fn().mockResolvedValue({ id: 7, name: 'User Tenant' }), }), })) vi.doMock('@/lib/email/tenant-email-service', () => ({ sendTestEmail: vi.fn().mockResolvedValue({ success: true, messageId: 'test-789', logId: 3, }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 7, recipientEmail: 'test@example.com', }), }) const response = await POST(req) // Should have access (tenant ID 7 matches) expect(response.status).not.toBe(403) }) }) describe('Input Validation', () => { it('requires tenantId', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, isSuperAdmin: true }, }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ recipientEmail: 'test@example.com', // Missing tenantId }), }) const response = await POST(req) expect(response.status).toBe(400) const body = await response.json() expect(body.error).toContain('tenantId') }) it('requires recipientEmail', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, isSuperAdmin: true }, }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, // Missing recipientEmail }), }) const response = await POST(req) expect(response.status).toBe(400) const body = await response.json() expect(body.error).toContain('recipientEmail') }) it('validates email format', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, isSuperAdmin: true }, }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'invalid-email', // Invalid format }), }) const response = await POST(req) expect(response.status).toBe(400) const body = await response.json() expect(body.error).toContain('E-Mail') }) it('validates tenantId is numeric', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, isSuperAdmin: true }, }), }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 'not-a-number', recipientEmail: 'test@example.com', }), }) const response = await POST(req) expect(response.status).toBe(400) const body = await response.json() expect(body.error).toContain('Zahl') }) }) describe('Rate Limiting', () => { it('includes rate limit headers', async () => { vi.resetModules() vi.doMock('payload', () => ({ getPayload: vi.fn().mockResolvedValue({ auth: vi.fn().mockResolvedValue({ user: { id: 1, isSuperAdmin: true }, }), findByID: vi.fn().mockResolvedValue({ id: 1, name: 'Test Tenant' }), }), })) vi.doMock('@/lib/email/tenant-email-service', () => ({ sendTestEmail: vi.fn().mockResolvedValue({ success: true, messageId: 'test-rate', logId: 1, }), })) const { POST } = await import('@/app/(payload)/api/test-email/route') const token = generateTestCsrfToken('test-csrf-secret-for-testing') const req = createMockRequest({ method: 'POST', url: 'https://test.example.com/api/test-email', headers: { 'content-type': 'application/json', origin: 'https://test.example.com', 'X-CSRF-Token': token, }, cookies: { 'csrf-token': token, }, ip: randomIp(), }) Object.defineProperty(req, 'json', { value: vi.fn().mockResolvedValue({ tenantId: 1, recipientEmail: 'test@example.com', }), }) const response = await POST(req) // Should include rate limit headers on success if (response.status === 200) { expect(response.headers.has('X-RateLimit-Limit')).toBe(true) expect(response.headers.has('X-RateLimit-Remaining')).toBe(true) } }) }) }) })