/** * Security Test Utilities * * Helper functions for testing security features. * Provides mock request builders, CSRF token utilities, and environment setup. */ import { NextRequest, NextResponse } from 'next/server' import { createHmac, randomBytes } from 'crypto' // ============================================================================ // CSRF Token Utilities // ============================================================================ const DEFAULT_CSRF_SECRET = 'test-csrf-secret-for-testing-only' /** * Generate a valid CSRF token for testing */ export function generateTestCsrfToken(secret: string = DEFAULT_CSRF_SECRET): string { const timestamp = Date.now().toString(36) const random = randomBytes(16).toString('hex') const data = `${timestamp}:${random}` const hmac = createHmac('sha256', secret) hmac.update(data) const signature = hmac.digest('hex').substring(0, 16) return `${data}:${signature}` } /** * Generate an expired CSRF token for testing */ export function generateExpiredCsrfToken(secret: string = DEFAULT_CSRF_SECRET): string { // Use timestamp from 2 hours ago const timestamp = (Date.now() - 2 * 60 * 60 * 1000).toString(36) const random = randomBytes(16).toString('hex') const data = `${timestamp}:${random}` const hmac = createHmac('sha256', secret) hmac.update(data) const signature = hmac.digest('hex').substring(0, 16) return `${data}:${signature}` } /** * Generate an invalid CSRF token (wrong signature) */ export function generateInvalidCsrfToken(): string { const timestamp = Date.now().toString(36) const random = randomBytes(16).toString('hex') return `${timestamp}:${random}:invalidsignature` } // ============================================================================ // Mock Request Builders // ============================================================================ export interface MockRequestOptions { method?: string url?: string headers?: Record body?: unknown cookies?: Record ip?: string } /** * Create a mock NextRequest for testing */ export function createMockRequest(options: MockRequestOptions = {}): NextRequest { const { method = 'GET', url = 'https://test.example.com/api/test', headers = {}, ip, } = options const requestHeaders = new Headers() // Set IP headers if provided if (ip) { requestHeaders.set('x-forwarded-for', ip) requestHeaders.set('x-real-ip', ip) } // Set custom headers Object.entries(headers).forEach(([key, value]) => { requestHeaders.set(key, value) }) const request = new NextRequest(url, { method, headers: requestHeaders, }) // Mock cookies if (options.cookies) { Object.defineProperty(request, 'cookies', { value: { get: (name: string) => { const value = options.cookies?.[name] return value ? { value } : undefined }, getAll: () => Object.entries(options.cookies || {}).map(([name, value]) => ({ name, value })), }, }) } return request } /** * Create a mock request with CSRF protection */ export function createCsrfProtectedRequest( method: string, options: Omit = {}, ): NextRequest { const token = generateTestCsrfToken() return createMockRequest({ ...options, method, headers: { ...options.headers, 'X-CSRF-Token': token, origin: 'https://test.example.com', }, cookies: { ...options.cookies, 'csrf-token': token, }, }) } /** * Create a mock request simulating server-to-server call */ export function createServerToServerRequest( method: string, options: Omit = {}, ): NextRequest { return createMockRequest({ ...options, method, headers: { ...options.headers, authorization: 'Bearer test-api-token', 'content-type': 'application/json', // No origin header for server-to-server }, }) } /** * Create a mock request from specific IP */ export function createRequestFromIp(ip: string, options: MockRequestOptions = {}): NextRequest { return createMockRequest({ ...options, ip, }) } // ============================================================================ // Environment Setup // ============================================================================ export interface SecurityTestEnv { CSRF_SECRET?: string PAYLOAD_SECRET?: string BLOCKED_IPS?: string SEND_EMAIL_ALLOWED_IPS?: string ADMIN_ALLOWED_IPS?: string WEBHOOK_ALLOWED_IPS?: string REDIS_HOST?: string REDIS_PORT?: string } /** * Setup environment variables for security tests */ export function setupSecurityEnv(env: SecurityTestEnv): void { Object.entries(env).forEach(([key, value]) => { if (value !== undefined) { process.env[key] = value } }) } /** * Clear security-related environment variables */ export function clearSecurityEnv(): void { const securityEnvVars = [ 'CSRF_SECRET', 'BLOCKED_IPS', 'SEND_EMAIL_ALLOWED_IPS', 'ADMIN_ALLOWED_IPS', 'WEBHOOK_ALLOWED_IPS', ] securityEnvVars.forEach((key) => { delete process.env[key] }) } // ============================================================================ // Response Helpers // ============================================================================ /** * Extract rate limit info from response headers */ export function extractRateLimitInfo(response: NextResponse): { limit: number | null remaining: number | null reset: number | null retryAfter: number | null } { return { limit: response.headers.get('X-RateLimit-Limit') ? parseInt(response.headers.get('X-RateLimit-Limit')!, 10) : null, remaining: response.headers.get('X-RateLimit-Remaining') ? parseInt(response.headers.get('X-RateLimit-Remaining')!, 10) : null, reset: response.headers.get('X-RateLimit-Reset') ? parseInt(response.headers.get('X-RateLimit-Reset')!, 10) : null, retryAfter: response.headers.get('Retry-After') ? parseInt(response.headers.get('Retry-After')!, 10) : null, } } /** * Parse JSON response body */ export async function parseJsonResponse(response: NextResponse): Promise { const text = await response.text() return JSON.parse(text) as T } // ============================================================================ // Rate Limit Testing // ============================================================================ /** * Make multiple requests to test rate limiting */ export async function exhaustRateLimit( requestFn: () => Promise, maxRequests: number, ): Promise { const responses: NextResponse[] = [] for (let i = 0; i <= maxRequests; i++) { responses.push(await requestFn()) } return responses } // ============================================================================ // Assertion Helpers // ============================================================================ /** * Assert response has security headers */ export function assertSecurityHeaders(response: NextResponse): void { const headers = response.headers // Rate limit headers should be present const hasRateLimitHeaders = headers.has('X-RateLimit-Limit') || headers.has('X-RateLimit-Remaining') if (!hasRateLimitHeaders) { throw new Error('Response missing rate limit headers') } } /** * Assert response is rate limited (429) */ export function assertRateLimited(response: NextResponse): void { if (response.status !== 429) { throw new Error(`Expected 429 status, got ${response.status}`) } if (!response.headers.has('Retry-After')) { throw new Error('Rate limited response missing Retry-After header') } } /** * Assert response is CSRF blocked (403) */ export function assertCsrfBlocked(response: NextResponse): void { if (response.status !== 403) { throw new Error(`Expected 403 status for CSRF block, got ${response.status}`) } } /** * Assert response is IP blocked (403) */ export function assertIpBlocked(response: NextResponse): void { if (response.status !== 403) { throw new Error(`Expected 403 status for IP block, got ${response.status}`) } } // ============================================================================ // Test Data Generators // ============================================================================ /** * Generate a random IP address */ export function randomIp(): string { return `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}` } /** * Generate a unique test identifier */ export function uniqueTestId(prefix: string = 'test'): string { return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` } /** * Generate test email */ export function testEmail(domain: string = 'test.example.com'): string { return `${uniqueTestId('user')}@${domain}` } // ============================================================================ // Mock Payload Instance // ============================================================================ export interface MockPayloadInstance { auth: ReturnType find: ReturnType create: ReturnType update: ReturnType delete: ReturnType login: ReturnType } /** * Create a mock Payload instance for testing */ export function createMockPayload( vi: typeof import('vitest').vi, ): MockPayloadInstance { return { auth: vi.fn().mockResolvedValue({ user: null }), find: vi.fn().mockResolvedValue({ docs: [], totalDocs: 0 }), create: vi.fn().mockResolvedValue({ id: 1 }), update: vi.fn().mockResolvedValue({ id: 1 }), delete: vi.fn().mockResolvedValue({ id: 1 }), login: vi.fn().mockResolvedValue({ user: { id: 1, email: 'test@example.com' }, token: 'mock-token', exp: Math.floor(Date.now() / 1000) + 7200, }), } }