Test, monitor, and operate
Use test keys and cover failures
Exercise deterministic success and failure without polluting production analytics or weakening production hostname controls.
8 minute lesson
You cannot test against a real challenge. Its outcome is not deterministic, and pointing automated tests at a production widget pollutes its analytics. Cloudflare provides documented Turnstile test keys for predictable automated outcomes.
The test keys
Site keys for the widget:
1x00000000000000000000AAalways passes2x00000000000000000000ABalways blocks3x00000000000000000000FFforces an interactive challenge
Secret keys for Siteverify:
1x0000000000000000000000000000000AAalways passes2x0000000000000000000000000000000AAalways fails3x0000000000000000000000000000000AAreturns a “token already spent” error
The test site keys issue the dummy token XXXX.DUMMY.TOKEN.XXXX. Use them in local and test environments, never the production secret. Your production widget keeps its hostname restrictions, and your test runs stay out of its analytics.
Cover the failure paths
The success path is the one you already tested by hand. The failures are where bots and bugs live. Cover missing token, invalid secret, expired or duplicate token, unexpected hostname, Siteverify outage, and application validation failure after Turnstile success.
The always-fails secret makes the rejection path a one-line setup:
test('failed verification performs no side effect', async () => {
const response = await handleSubmit(formWith('XXXX.DUMMY.TOKEN.XXXX'), {
TURNSTILE_SECRET_KEY: '2x0000000000000000000000000000000AA',
})
assert.equal(response.status, 403)
assert.equal(sentEmails.length, 0)
})
The second assertion is the important one. Verify no protected side effect occurs on any failed path: no email sent, no row inserted, no webhook fired. A handler that returns 403 after doing the work has already lost.
For the Siteverify outage case, stub fetch to throw and confirm your handler fails closed instead of waving the request through.
One browser test, many server tests
Add one browser test for the form flow with the always-passes site key, and server tests for every Siteverify response class your handler understands. The browser test proves the wiring: script loads, token lands in cf-turnstile-response, form submits. The server tests prove the decisions.
Run the suite, then break your own handler on purpose by removing the Siteverify call. If every test still passes, your tests are checking the widget, not the protection.
Lesson completed