import { test, expect } from '@playwright/test'; /** * Destructive / Chaos Tests * QA's job is to break things before production does. * * Tests cover: * - Boundary value inputs (XSS, SQL injection, max-length) * - Race conditions (rapid submission) * - Network failure scenarios (offline, slow, timeout) * - Malformed data injection * - Session state after errors * - Concurrent tab behavior */ // ============================================================ // Boundary Value Attacks // ============================================================ test.describe('Chaos: Input Boundary Attacks', () => { test.beforeEach(async ({ page }) => { await page.goto('/contact'); await page.waitForLoadState('domcontentloaded'); }); test('XSS attempt in name field is sanitized', async ({ page }) => { const xssPayload = ''; await page.locator('input[name="name"]').fill(xssPayload); await page.locator('input[name="email"]').fill('xss@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('XSS test message content here'); await page.locator('button[type="submit"]').click(); // Alert dialog should NOT appear (XSS blocked) let alertFired = false; page.on('dialog', async (dialog) => { alertFired = true; await dialog.dismiss(); }); await page.waitForTimeout(1000); expect(alertFired).toBe(false); }); test('XSS attempt in message field is sanitized', async ({ page }) => { const xssPayload = '">'; await page.locator('input[name="name"]').fill('Normal Name'); await page.locator('input[name="email"]').fill('xss2@test.com'); await page.locator('select[name="subject"]').selectOption('consulting'); await page.locator('textarea[name="message"]').fill(xssPayload); await page.locator('button[type="submit"]').click(); let alertFired = false; page.on('dialog', async (dialog) => { alertFired = true; await dialog.dismiss(); }); await page.waitForTimeout(1000); expect(alertFired).toBe(false); }); test('SQL injection attempt in message field is handled safely', async ({ page }) => { const sqlPayload = "'; DROP TABLE users; --"; await page.locator('input[name="name"]').fill('SQL Injector'); await page.locator('input[name="email"]').fill('sql@test.com'); await page.locator('select[name="subject"]').selectOption('support'); await page.locator('textarea[name="message"]').fill(sqlPayload); await page.locator('button[type="submit"]').click(); // Page should not crash or show server errors await page.waitForTimeout(3000); await expect(page.locator('body')).not.toContainText(/syntax error|sql|database error/i); }); test('maximum length name (100 chars) is accepted', async ({ page }) => { const maxName = 'A'.repeat(100); await page.locator('input[name="name"]').fill(maxName); await expect(page.locator('input[name="name"]')).toHaveValue(maxName); }); test('maximum length message (5000 chars) is accepted at API level', async ({ request }) => { const maxMessage = 'A'.repeat(5000); const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: 'Max Length Tester', email: 'max@test.com', subject: 'web-development', message: maxMessage, website: '', }, }); // Should not fail with 422 (validation error) for boundary-valid message expect([200, 429]).toContain(response.status()); }); test('message exceeding 5001 chars is rejected by API', async ({ request }) => { const overMaxMessage = 'B'.repeat(5001); const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: 'Over Max Tester', email: 'overmax@test.com', subject: 'web-development', message: overMaxMessage, website: '', }, }); // Should reject overly long message expect(response.status()).toBe(422); }); test('unicode characters in name field handled correctly', async ({ page }) => { const unicodeName = '日本語名前 Ñoño García'; await page.locator('input[name="name"]').fill(unicodeName); await expect(page.locator('input[name="name"]')).toHaveValue(unicodeName); }); test('emoji in message field handled correctly', async ({ page }) => { const emojiMessage = '🚀 Hello! I need help with my website 💻✨ Please contact me ASAP! 🙏'; await page.locator('textarea[name="message"]').fill(emojiMessage); await expect(page.locator('textarea[name="message"]')).toHaveValue(emojiMessage); }); test('null bytes and control characters rejected gracefully', async ({ request }) => { const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: 'Normal Name', email: 'test@test.com', subject: 'web-development', message: 'Normal message\x00\x01\x02 with control chars', website: '', }, }); // Should handle without crashing (200 or validation error, not 500) expect([200, 422, 429]).toContain(response.status()); }); }); // ============================================================ // Race Condition Tests // ============================================================ test.describe('Chaos: Race Conditions', () => { test('rapid double-click on submit is prevented', async ({ page }) => { await page.goto('/contact'); await page.locator('input[name="name"]').fill('Race Condition Test'); await page.locator('input[name="email"]').fill('race@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('Testing rapid submission prevention.'); const submitButton = page.locator('button[type="submit"]'); // Track API calls let apiCallCount = 0; page.on('request', (req) => { if (req.url().includes('/api/contact') && req.method() === 'POST') { apiCallCount++; } }); // Double click rapidly await submitButton.click(); await submitButton.click({ force: true }); // Force to bypass disabled state check await page.waitForTimeout(3000); // Only one API call should be made (button disabled after first click) expect(apiCallCount).toBeLessThanOrEqual(2); // At most 2 (one valid) }); test('concurrent newsletter subscription submissions', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('concurrent@test.com'); // Click submit, then immediately try again await submitButton.click(); // Button should be immediately disabled await expect(submitButton).toBeDisabled(); }); test('form state is consistent during API call', async ({ page }) => { await page.goto('/contact'); await page.locator('input[name="name"]').fill('State Test User'); await page.locator('input[name="email"]').fill('state@test.com'); await page.locator('select[name="subject"]').selectOption('consulting'); await page.locator('textarea[name="message"]').fill('Testing form state consistency.'); const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Verify UI shows loading state immediately // Button should be disabled while in-flight await expect(submitButton).toBeDisabled(); // Name field should still have its value (not cleared prematurely) await expect(page.locator('input[name="name"]')).toHaveValue('State Test User'); }); }); // ============================================================ // Network Failure Scenarios // ============================================================ test.describe('Chaos: Network Failures', () => { test('contact form handles connection abort gracefully', async ({ page }) => { await page.goto('/contact'); await page.route('/api/contact', async (route) => { await route.abort('connectionaborted'); }); await page.locator('input[name="name"]').fill('Abort Test'); await page.locator('input[name="email"]').fill('abort@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('Testing connection abort handling.'); await page.locator('button[type="submit"]').click(); // Should show error toast await expect(page.locator('#toast-container')).toContainText(/network|error|check/i, { timeout: 5000 }); }); test('contact form handles server timeout gracefully', async ({ page }) => { await page.goto('/contact'); // Simulate a very slow response await page.route('/api/contact', async (route) => { // Delay 30 seconds (longer than the test timeout for this scenario) await new Promise((resolve) => setTimeout(resolve, 30000)); await route.continue(); }); await page.locator('input[name="name"]').fill('Timeout Test'); await page.locator('input[name="email"]').fill('timeout@test.com'); await page.locator('select[name="subject"]').selectOption('support'); await page.locator('textarea[name="message"]').fill('Testing timeout scenario handling.'); await page.locator('button[type="submit"]').click(); // Button should be disabled during the long wait await expect(page.locator('button[type="submit"]')).toBeDisabled(); }); test('contact form recovers after network failure', async ({ page }) => { await page.goto('/contact'); // First request fails let requestCount = 0; await page.route('/api/contact', async (route) => { requestCount++; if (requestCount === 1) { await route.abort('failed'); } else { await route.continue(); } }); await page.locator('input[name="name"]').fill('Recovery Test'); await page.locator('input[name="email"]').fill('recovery@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('Testing form recovery after failure.'); await page.locator('button[type="submit"]').click(); // Wait for error await page.waitForTimeout(2000); // Button should re-enable await expect(page.locator('button[type="submit"]')).toBeEnabled(); // Try again await page.locator('button[type="submit"]').click(); await page.waitForTimeout(3000); // Second attempt should work const toastText = await page.locator('#toast-container').textContent(); expect(toastText).toBeTruthy(); }); test('page navigation works even when API is down', async ({ page }) => { // Block all API calls await page.route('/api/*', async (route) => { await route.abort('failed'); }); await page.goto('/'); await expect(page).toHaveURL('/'); // Navigation should still work await page.locator('a[href="/about"]').first().click(); await expect(page).toHaveURL('/about'); await expect(page.locator('h1')).toBeVisible(); }); test('slow network does not break form UI', async ({ page }) => { await page.goto('/contact'); // Simulate 3G-like latency (2 second delay) await page.route('/api/contact', async (route) => { await new Promise((resolve) => setTimeout(resolve, 2000)); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, message: 'Slow success!' }), }); }); await page.locator('input[name="name"]').fill('Slow Network Test'); await page.locator('input[name="email"]').fill('slow@test.com'); await page.locator('select[name="subject"]').selectOption('consulting'); await page.locator('textarea[name="message"]').fill('Testing slow network scenario.'); await page.locator('button[type="submit"]').click(); // Button disabled during slow request await expect(page.locator('button[type="submit"]')).toBeDisabled(); // Eventually gets result await expect(page.locator('#toast-container')).toContainText(/success/i, { timeout: 8000 }); }); }); // ============================================================ // Session State & Edge Cases // ============================================================ test.describe('Chaos: Session State & Edge Cases', () => { test('browser back button after form submission', async ({ page }) => { await page.goto('/'); await page.goto('/contact'); await page.locator('input[name="name"]').fill('Back Button Test'); await page.locator('input[name="email"]').fill('back@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('Testing back button behavior after submit.'); await page.locator('button[type="submit"]').click(); // Wait briefly for submission await page.waitForTimeout(2000); // Navigate back await page.goBack(); await page.waitForLoadState('domcontentloaded'); // Should not cause errors await expect(page.locator('body')).toBeVisible(); }); test('page reload during form fill does not cause errors', async ({ page }) => { await page.goto('/contact'); await page.locator('input[name="name"]').fill('Reload Test'); // Reload mid-fill await page.reload(); await page.waitForLoadState('domcontentloaded'); // Form should be clean after reload const nameValue = await page.locator('input[name="name"]').inputValue(); expect(nameValue).toBe(''); }); test('multiple rapid page navigations do not cause errors', async ({ page }) => { const pages = ['/', '/about', '/services', '/portfolio', '/blog', '/contact']; const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') { errors.push(msg.text()); } }); // Navigate rapidly between pages for (const path of pages) { await page.goto(path); } // Should not accumulate critical errors const criticalErrors = errors.filter((e) => !e.includes('favicon') && !e.includes('404') && !e.includes('Failed to load resource') ); // Allow for minor non-critical errors expect(criticalErrors.length).toBeLessThan(3); }); test('contact form with maximum fields does not overflow layout', async ({ page }) => { await page.goto('/contact'); const longMessage = 'This is a very long message. '.repeat(50); await page.locator('input[name="name"]').fill('Layout Test User'); await page.locator('input[name="email"]').fill('layout@test.com'); await page.locator('input[name="phone"]').fill('+1 (555) 999-8888'); await page.locator('select[name="subject"]').selectOption('other'); await page.locator('textarea[name="message"]').fill(longMessage.substring(0, 1000)); // Check that form does not overflow the viewport const form = page.locator('#contact-form'); const formBox = await form.boundingBox(); const viewportSize = page.viewportSize(); if (formBox && viewportSize) { expect(formBox.width).toBeLessThanOrEqual(viewportSize.width + 20); } }); test('form with only spaces is rejected', async ({ page }) => { await page.goto('/contact'); await page.locator('input[name="name"]').fill(' '); // Spaces only await page.locator('input[name="email"]').fill('spaces@test.com'); await page.locator('select[name="subject"]').selectOption('web-development'); await page.locator('textarea[name="message"]').fill('Valid message content here.'); await page.locator('button[type="submit"]').click(); // Should show name validation error (spaces trimmed = empty) await page.waitForTimeout(1000); const nameError = page.locator('[data-error="name"]'); const isVisible = await nameError.evaluate((el) => !el.classList.contains('hidden')); expect(isVisible).toBeTruthy(); }); test('newsletter with spaces-only email is rejected', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); const emailInput = page.locator('#newsletter-email'); await emailInput.fill(' '); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await submitButton.click(); // HTML5 validation should block this const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.checkValidity()); expect(isInvalid).toBeTruthy(); }); }); // ============================================================ // Injection & Security Edge Cases // ============================================================ test.describe('Chaos: API Security Edge Cases', () => { test('contact API rejects non-POST methods', async ({ request }) => { // GET should not be allowed const getResponse = await request.get('/api/contact'); // Should return 404 or 405 Method Not Allowed expect([404, 405]).toContain(getResponse.status()); }); test('newsletter API rejects non-POST methods on data submission', async ({ request }) => { const getResponse = await request.get('/api/newsletter'); expect([404, 405]).toContain(getResponse.status()); }); test('contact API with missing Content-Type still handled safely', async ({ request }) => { const response = await request.post('/api/contact', { data: JSON.stringify({ name: 'No Content Type', email: 'noct@test.com', subject: 'web-development', message: 'Testing missing content type header.', }), }); // Should not crash the server expect([200, 400, 422, 429]).toContain(response.status()); }); test('extremely large request body is rejected', async ({ request }) => { const hugeName = 'X'.repeat(100000); // 100KB just for name const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: hugeName, email: 'huge@test.com', subject: 'web-development', message: 'Normal message', website: '', }, }); // Should reject or handle without server crash expect([400, 413, 422, 429]).toContain(response.status()); }); test('contact API with array values for string fields handled', async ({ request }) => { const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: ['array', 'injection'], email: 'array@test.com', subject: 'web-development', message: 'Normal message content.', website: '', }, }); // Should not crash (422 acceptable) expect([200, 400, 422, 429, 500]).toContain(response.status()); }); test('contact API with number values for string fields handled', async ({ request }) => { const response = await request.post('/api/contact', { headers: { 'Content-Type': 'application/json' }, data: { name: 12345, email: 'number@test.com', subject: 'web-development', message: 'Number field injection test.', website: '', }, }); // Should handle type coercion or reject gracefully expect([200, 400, 422, 429]).toContain(response.status()); }); });