Tutorials

Redis pour le diagnostic du TTL des tokens CAPTCHA dans vos workflows QA

Un test qui passe le matin et échoue l'après-midi a rarement un problème de CAPTCHA : il a un problème d'horloge. Le token est arrivé, mais le backend l'a vérifié après la fin de sa fenêtre de validité, sans horodatage pour le prouver. Redis règle ce point : trois champs datés par exécution, une expiration native sur la clé.

Périmètre : ces schémas de diagnostic sont destinés à vos propres environnements QA ou à des environnements explicitement autorisés.


Ce que la durée de vie d'un token change en QA

Les fenêtres de validité sont courtes — de l'ordre de deux minutes pour reCAPTCHA, environ cinq minutes pour Cloudflare Turnstile — et tout ce qui s'intercale avant l'appel de vérification consomme cette marge : un sleep dans une fixture, un runner CI saturé, une capture d'écran.

Un résultat est à usage unique et lié à sa page : l'enjeu n'est pas de le conserver, mais de répondre à quatre questions à chaque exécution — quand la tâche est partie, quand le résultat est revenu, quelle marge restait, ce qu'a répondu le backend.


Le schéma TTL minimal à écrire dans Redis

Sept champs suffisent. Ne stockez jamais le token complet : un préfixe identifie l'enregistrement sans garder de secret exploitable, minimisation RGPD comprise.

Champ Rôle Exemple
task_id Tâche CaptchaAI 185734920
issued_at Création de la tâche 2026-04-27T08:15:11Z
received_at Arrivée du résultat 2026-04-27T08:15:29Z
expires_at Borne d'expiration QA 2026-04-27T08:17:09Z
test_run_id Exécution de test checkout-regression-1042
environment Environnement staging
verification_result Verdict du backend accepted / expired / invalid

Étape 1 : écrire l'enregistrement à la réception du résultat

La clé porte l'identifiant de l'exécution et son ex reprend la fenêtre à tester : Redis expire l'enregistrement en même temps que le token, et TTL <clé> donne la marge restante en secondes.

from __future__ import annotations

from datetime import datetime, timedelta, timezone
import json
import redis


r = redis.Redis(host="localhost", port=6379, decode_responses=True)


def token_record_key(test_run_id: str) -> str:
    return f"qa:captcha:ttl:{test_run_id}"


def store_ttl_record(*, test_run_id: str, task_id: str, received_token: str, ttl_seconds: int) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "task_id": task_id,
        "token_preview": received_token[:16],
        "issued_at": now.isoformat(),
        "expires_at": (now + timedelta(seconds=ttl_seconds)).isoformat(),
        "verification_result": "pending",
        "environment": "staging",
    }
    key = token_record_key(test_run_id)
    r.set(key, json.dumps(payload), ex=ttl_seconds)
    return key

Étape 2 : rattacher la résolution CaptchaAI à l'exécution

Gardez les deux valeurs de retour : c'est le couple task_id + résultat qui rend la mesure corrélable.

import requests
import time


API_KEY = "YOUR_API_KEY"


def submit_and_poll(page_url: str, sitekey: str) -> tuple[str, str]:
    submit = requests.post(
        "https://ocr.captchaai.com/in.php",
        data={
            "key": API_KEY,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url,
            "json": 1,
        },
        timeout=30,
    )
    submit.raise_for_status()
    submit_json = submit.json()
    if submit_json.get("status") != 1:
        raise RuntimeError(submit_json.get("request", "submit failed"))

    task_id = submit_json["request"]
    for _ in range(30):
        time.sleep(5)
        result = requests.get(
            "https://ocr.captchaai.com/res.php",
            params={"key": API_KEY, "action": "get", "id": task_id, "json": 1},
            timeout=30,
        )
        result.raise_for_status()
        result_json = result.json()
        if result_json.get("status") == 1:
            return task_id, result_json["request"]

    raise TimeoutError("CaptchaAI polling timed out")

Le plafond de la boucle est une donnée : si vos exécutions le frôlent, la marge est déjà consommée. Regardez alors les threads de votre plan — BASIC ($15/mois, 5 threads) sature vite en parallèle.


Étape 3 : pousser le backend jusqu'à la borne d'expiration

Une exécution utile fait deux passages : une vérification immédiate, puis une vérification retardée après la borne interne.

def verify_in_staging(token: str, test_run_id: str, delay_seconds: int = 0) -> dict:
    if delay_seconds:
        time.sleep(delay_seconds)

    response = requests.post(
        "https://staging.example-app.test/qa/captcha/verify",
        json={
            "token": token,
            "testRunId": test_run_id,
            "environment": "staging",
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()

C'est le second passage qui trouve les bugs : un backend qui accepte un token trente secondes après la borne ne valide plus rien. Écrivez le verdict dans verification_result avant l'expiration de la clé.


Cas pratique : une régression de checkout côté CI

Une équipe belge exécute sa suite sur un staging hébergé en Europe. En local tout passe ; sur le runner CI, un paiement sur trois est refusé. Les enregistrements TTL tranchent : la marge restante tombe d'environ 90 s en local à moins de 10 s sur le runner, où le CI insère une capture Playwright avant la vérification. Le correctif ne touche ni CaptchaAI ni Redis : la capture passe après.


Dépannage

Problème Cause Correctif
Chronologie d'un échec impossible à reconstituer Horodatages manquants ou horloges divergentes Écrire issued_at, received_at, expires_at en UTC
Clés disparues avant la fin du test ex calé sans marge Allonger la fenêtre QA interne
Verdicts opposés sur deux exécutions Fixtures divergentes Journaliser test_run_id et environment
Redis dit « accepted », le produit échoue Traces non corrélées Propager le même identifiant jusqu'au backend
Marge restante proche de zéro sleep fixes ou threads saturés Retirer les attentes inutiles, vérifier les threads

Questions fréquentes

Combien de temps un token reste-t-il exploitable ?

Quelques minutes au plus, selon le type. Ne figez pas ces valeurs dans vos assertions : mesurez la marge observée et testez autour.

Faut-il stocker le token complet dans Redis ?

Non. Un préfixe suffit à identifier l'enregistrement, et un résultat CAPTCHA reste un secret de courte durée, à tenir hors des logs.

Que faire si le backend accepte un token expiré ?

Traitez-le comme un défaut bloquant : reproduisez-le avec un délai fixe dans verify_in_staging, puis contrôlez la réponse de l'API de vérification.

Cette méthode couvre-t-elle hCaptcha ?

Non : hCaptcha et FunCaptcha ne sont pas pris en charge, GeeTest v4 est à venir. Elle couvre reCAPTCHA v2 et v3, Cloudflare Turnstile, GeeTest v3 et les CAPTCHA image ou texte.


Guides connexes sûrs

Ouvrez un compte CaptchaAI et mesurez la marge d'expiration réelle sur votre staging dès la prochaine exécution.

Les commentaires sont désactivés pour cet article.