/** * Unit tests for Contact API route * These tests focus on testing the business logic without complex mocking */ import { POST } from './route' import { getPayload } from 'payload' const mockCreate = jest.fn() // Mock the logger to avoid complex setup jest.mock('@/lib/logger', () => ({ createComponentLogger: () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), }) })) // WHY mock the resolved relative path instead of the '@payload-config' alias: // next/jest's SWC transform rewrites the tsconfig path alias to a real // relative specifier at transform time (Jest itself doesn't understand // tsconfig `paths`), so a `jest.mock('@payload-config', ...)` never actually // intercepts what route.ts ends up requiring — it silently falls through to // the real proto/src/payload.config.ts (mongooseAdapter, Users/LegalPages/etc // imports), which needs a live MongoDB and PAYLOAD_SECRET, exactly what these // tests avoid. The route only ever passes this value through to the (also // mocked) getPayload(), so its actual shape doesn't matter here. jest.mock('../../../payload.config', () => ({ __esModule: true, default: {} })) jest.mock('payload', () => ({ getPayload: jest.fn(async () => ({ create: mockCreate })), })) describe('/api/contact Unit Tests', () => { beforeEach(() => { mockCreate.mockReset() ;(getPayload as jest.Mock).mockClear() }) describe('Payload persistence', () => { const validData = { name: 'Test User', email: 'test@example.com', subject: 'Test Subject', message: 'This is a test message with enough content', gdprConsent: true, } it('persists a valid submission before reporting success', async () => { mockCreate.mockResolvedValue({ id: 'submission-123', createdAt: '2026-09-12T00:00:00.000Z' }) const request = { headers: new Headers({ 'x-forwarded-for': 'persistence-success' }), json: async () => validData, } as any const response = await POST(request) const body = await response.json() expect(response.status).toBe(200) expect(body.submissionId).toBe('submission-123') expect(mockCreate).toHaveBeenCalledWith({ collection: 'contact-submissions', data: expect.objectContaining({ ...validData, status: 'new', }), }) }) it('returns a server error when persistence fails', async () => { mockCreate.mockRejectedValue(new Error('MongoDB unavailable')) const request = { headers: new Headers({ 'x-forwarded-for': 'persistence-failure' }), json: async () => validData, } as any const response = await POST(request) expect(response.status).toBe(500) expect(mockCreate).toHaveBeenCalledTimes(1) }) it('rejects an over-long message before persisting', async () => { const longMessage = { ...validData, message: 'x'.repeat(5001) } const request = { headers: new Headers({ 'x-forwarded-for': 'too-long' }), json: async () => longMessage, } as any const response = await POST(request) expect(response.status).toBe(400) expect(mockCreate).not.toHaveBeenCalled() }) }) // TC-001: Email Format Validation Test (ZEE-48) describe('Input validation logic', () => { it('should validate required fields', () => { const validData = { name: 'Test User', email: 'test@example.com', subject: 'Test Subject', message: 'This is a test message with enough content', gdprConsent: true } // Test individual field validation logic expect(validData.name.length).toBeGreaterThan(1) expect(validData.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) expect(validData.subject.length).toBeGreaterThan(2) expect(validData.message.length).toBeGreaterThan(9) expect(validData.gdprConsent).toBe(true) }) it('TC-001: should detect invalid email formats', () => { const invalidEmails = [ 'invalid-email', 'test@', '@example.com', 'test.example.com', '' ] const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ invalidEmails.forEach(email => { expect(email).not.toMatch(emailRegex) }) }) it('should validate field lengths', () => { const testCases = [ { field: 'name', value: 'T', minLength: 2, valid: false }, { field: 'name', value: 'Test User', minLength: 2, valid: true }, { field: 'subject', value: 'Te', minLength: 3, valid: false }, { field: 'subject', value: 'Test Subject', minLength: 3, valid: true }, { field: 'message', value: 'Short', minLength: 10, valid: false }, { field: 'message', value: 'This is a longer message', minLength: 10, valid: true } ] testCases.forEach(testCase => { const isValid = testCase.value.length >= testCase.minLength expect(isValid).toBe(testCase.valid) }) }) }) describe('Spam detection logic', () => { it('should detect spam keywords', () => { const spamKeywords = [ 'free money', 'click here', 'buy now', 'urgent business', 'viagra', 'cheap', 'limited time', 'act now' ] const spamText = 'FREE MONEY CLICK HERE NOW BUY VIAGRA CHEAP URGENT BUSINESS PROPOSAL' let spamCount = 0 spamKeywords.forEach(keyword => { if (spamText.toLowerCase().includes(keyword.toLowerCase())) { spamCount++ } }) // Should detect multiple spam keywords expect(spamCount).toBeGreaterThan(3) }) it('should allow legitimate business content', () => { const legitimateText = 'Hello, I would like to buy your web hosting service. Can you provide more information about your business offerings?' const spamKeywords = [ 'free money', 'click here now', 'urgent business proposal', 'viagra', 'cheap pills', 'limited time offer' ] let spamCount = 0 spamKeywords.forEach(keyword => { if (legitimateText.toLowerCase().includes(keyword.toLowerCase())) { spamCount++ } }) // Should not trigger spam detection expect(spamCount).toBeLessThan(2) }) }) describe('Input sanitization logic', () => { it('should handle potentially dangerous characters', () => { const dangerousInput = 'Test User' // Simple sanitization check - removing script tags const sanitized = dangerousInput.replace(/]*>.*?<\/script>/gi, '') expect(sanitized).toBe('Test User') expect(sanitized).not.toContain('