# Turnstile Testing Guide **Complete testing strategies for E2E, unit, and integration tests** **Official Docs**: https://developers.cloudflare.com/turnstile/troubleshooting/testing/ --- ## Quick Reference: Dummy Credentials ### Sitekeys (Client-Side) ```typescript const TEST_SITEKEYS = { ALWAYS_PASS: '1x00000000000000000000AA', // Visible, always passes ALWAYS_BLOCK: '2x00000000000000000000AB', // Visible, always blocks ALWAYS_PASS_INVISIBLE: '1x00000000000000000000BB', // Invisible, always passes ALWAYS_BLOCK_INVISIBLE: '2x00000000000000000000BB',// Invisible, always blocks FORCE_INTERACTIVE: '3x00000000000000000000FF', // Visible, forces checkbox } ``` ### Secret Keys (Server-Side) ```typescript const TEST_SECRET_KEYS = { ALWAYS_PASS: '1x0000000000000000000000000000000AA', // success: true ALWAYS_FAIL: '2x0000000000000000000000000000000AA', // success: false TOKEN_SPENT: '3x0000000000000000000000000000000AA', // "already spent" error } ``` ### Dummy Token ```typescript const DUMMY_TOKEN = 'XXXX.DUMMY.TOKEN.XXXX' ``` **CRITICAL**: - Dummy sitekeys generate `XXXX.DUMMY.TOKEN.XXXX` - Dummy secret keys ONLY accept dummy token - Production secret keys REJECT dummy token - Real tokens FAIL with dummy secret keys --- ## Environment Detection Patterns ### Pattern 1: Request Headers ```typescript function isTestEnvironment(request: Request): boolean { return request.headers.get('x-test-environment') === 'true' } // Usage in Cloudflare Worker if (isTestEnvironment(request)) { secretKey = TEST_SECRET_KEYS.ALWAYS_PASS } ``` ### Pattern 2: IP Address ```typescript function isTestEnvironment(request: Request): boolean { const ip = request.headers.get('CF-Connecting-IP') || '' const testIPs = ['127.0.0.1', '::1', 'localhost'] return testIPs.includes(ip) } ``` ### Pattern 3: Query Parameter ```typescript function isTestEnvironment(request: Request): boolean { const url = new URL(request.url) return url.searchParams.get('test') === 'true' } ``` ### Pattern 4: Environment Variable ```typescript const sitekey = process.env.NODE_ENV === 'test' ? TEST_SITEKEYS.ALWAYS_PASS : process.env.TURNSTILE_SITE_KEY ``` --- ## Playwright Testing ### Setup ```typescript // playwright.config.ts export default defineConfig({ use: { baseURL: 'http://localhost:5173', extraHTTPHeaders: { 'x-test-environment': 'true', // Auto-use test credentials }, }, }) ``` ### Basic Test ```typescript // tests/contact-form.spec.ts import { test, expect } from '@playwright/test' test('submits contact form with Turnstile', async ({ page }) => { await page.goto('/contact') // Fill form await page.fill('input[name="email"]', 'test@example.com') await page.fill('textarea[name="message"]', 'Test message') // Turnstile auto-solves with dummy token in test mode await page.click('button[type="submit"]') // Verify success await expect(page.locator('.success-message')).toBeVisible() }) ``` ### Advanced: Multiple Scenarios ```typescript test('handles Turnstile failure gracefully', async ({ page, context }) => { // Override to use "always fail" sitekey await context.route('**/api.js', route => { const FAIL_SITEKEY = '2x00000000000000000000AB' // Inject failing sitekey route.continue() }) await page.goto('/contact') await page.fill('input[name="email"]', 'test@example.com') await page.click('button[type="submit"]') await expect(page.locator('.error-message')).toContainText('verification failed') }) ``` --- ## Cypress Testing ### Setup ```typescript // cypress.config.ts export default defineConfig({ e2e: { baseUrl: 'http://localhost:5173', setupNodeEvents(on, config) { // Set test header on all requests on('before:browser:launch', (browser, launchOptions) => { launchOptions.args.push('--disable-web-security') return launchOptions }) }, }, }) ``` ### Test Example ```typescript // cypress/e2e/turnstile.cy.ts describe('Turnstile Form', () => { beforeEach(() => { cy.intercept('**/*', (req) => { req.headers['x-test-environment'] = 'true' }) }) it('submits form successfully', () => { cy.visit('/contact') cy.get('input[name="email"]').type('test@example.com') cy.get('textarea[name="message"]').type('Test message') // Turnstile auto-solves in test mode cy.get('button[type="submit"]').click() cy.contains('Success').should('be.visible') }) }) ``` --- ## Jest / Vitest (React) ### Mock @marsidev/react-turnstile ```typescript // jest.setup.ts or vitest.setup.ts import React from 'react' jest.mock('@marsidev/react-turnstile', () => ({ Turnstile: ({ onSuccess }: { onSuccess: (token: string) => void }) => { // Auto-solve with dummy token React.useEffect(() => { onSuccess('XXXX.DUMMY.TOKEN.XXXX') }, [onSuccess]) return
}, })) ``` ### Component Test ```typescript // ContactForm.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { ContactForm } from './ContactForm' test('submits form with Turnstile', async () => { render(