cms.c2sgmbh/tests/int/security-api.int.spec.ts
Martin Porwoll fdc6876207 fix(ci): add CI stub to security integration tests
Ensure CSRF validation works normally during security API tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 13:27:44 +00:00

1195 lines
35 KiB
TypeScript

/**
* 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'
// Clear CI environment variable to ensure CSRF validation works normally during tests
vi.stubEnv('CI', '')
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: '<p>Test</p>',
}),
})
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)
}
})
})
})
})