Test, monitor, and operate
Use test keys and cover failures
Exercise deterministic success and failure without polluting production analytics or weakening production hostname controls.
You cannot write automated tests against a real challenge. Its outcome is not predictable, and pointing a test suite at a production widget pollutes its analytics. Cloudflare solves this with documented test keys that always behave the same way.
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 these keys 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
You already tested the success path by hand. The failures are where bots and bugs live. Cover a missing token, an invalid secret, an expired or duplicate token, an unexpected hostname, a Siteverify outage, and a form that fails your own validation after Turnstile passed.
The always-fails secret makes the rejection test a one-line setup:
test('failed verification performs no side effect', async () => {
const response = await handleSubmit(formWith('XXXX.DUMMY.TOKEN.XXXX'), {
TURNSTILE_SECRET: '2x0000000000000000000000000000000AA',
})
assert.equal(response.status, 403)
assert.equal(sentEmails.length, 0)
})
The second assertion is the one that matters. On every failed path, check that no protected side effect happened: 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. Then write server tests for every Siteverify response your handler understands. The browser test proves the wiring: the script loads, the token lands in cf-turnstile-response, the form submits. The server tests prove the decisions.
Run the suite. Then break your own handler on purpose by deleting the Siteverify call. If every test still passes, your tests are checking the widget, not the protection.
Lesson completed