Captcha in TestCafe-Tests lösen (Node.js-SDK)

testcafe captcha - How to Solve a CAPTCHA in TestCafe Tests (Node.js SDK)

Ein Captcha-Schritt in TestCafe fällt kürzer aus als in den meisten Frameworks, denn TestCafe führt Ihren Testcode ohnehin in Node aus. Sie rufen den Löser direkt aus der Testdatei auf und schreiben das Token anschließend mit einer ClientFunction in die Seite. Vorher gibt es allerdings eine Sache zu prüfen, und wer sie falsch beantwortet, verliert einen halben Tag: ob Ihr Lauf die native Automatisierung nutzt oder den alten Proxy mit URL-Umschreibung. Auf dem Proxy ist reCAPTCHA schon kaputt, bevor ein Löser überhaupt in seine Nähe kommt.

Was Sie brauchen

  • TestCafe 3.0 oder neuer und Node.js 18 oder neuer, dazu das CapSkip Node.js-SDK.
  • Ein Chromium-Browser, also Chrome oder Edge. Die native Automatisierung deckt Firefox und Safari nicht ab.
  • Die Seiten-URL des zu testenden Formulars und dessen Sitekey.
  • CapSkip im Local mode, wenn Testrunner und Löser auf derselben Maschine liegen, sonst im Server mode. Beide Modi finden Sie unter Verbindungseinstellungen.
# npm install capskip
npm install --save-dev testcafe
npm install capskip

Zuerst prüfen: native Automatisierung oder Proxy?

TestCafe hat zwei Wege, einen Browser zu steuern, und sie verhalten sich rund um Captchas völlig unterschiedlich. Der ursprüngliche Weg ist ein Web-Proxy namens hammerhead. Er sitzt zwischen Browser und Website, injiziert seine Automatisierungsskripte in jede Seite und schreibt jede URL in der Ressource so um, dass sie zurück auf den Proxy zeigt. Genau das hat TestCafe in die Lage versetzt, jeden Browser ohne Treiber zu unterstützen, und genau das macht auch reCAPTCHA kaputt.

Auf dem Proxy treten zwei Fehler auf, beide sind gegen hammerhead gemeldet und keiner davon lässt sich aus Ihrem Test heraus beheben. reCAPTCHA versucht, einen Web Worker vom Origin von Google zu starten, und der Browser verweigert das, weil der Origin des Dokuments jetzt Host und Port des Proxys selbst ist. Und Seiten, die durch den Proxy ausgeliefert werden, kommen jedes Mal mit einem reCAPTCHA-v3-Score von 0,1 zurück, was die meisten Websites unmittelbar als Bot werten.

Die native Automatisierung hat all das ersetzt. TestCafe steuert Chromium stattdessen über das DevTools-Protokoll, es gibt also keinen Proxy im Pfad und überhaupt kein Umschreiben von URLs. Sie kam in v2.5.0 als Experiment und ist seit v3.0.0 der Standard. Wenn Ihre Suite auf einem aktuellen TestCafe läuft und in Chrome ausgeführt wird, haben Sie sie bereits.

Der erste Schritt bei der Fehlersuche ist deshalb die Bestätigung, dass nichts sie abgeschaltet hat. TestCafe deaktiviert die native Automatisierung bei Firefox und Safari automatisch, und das CLI-Flag namens disable-native-automation samt seinem Pendant in der Konfigurationsdatei schaltet sie auch unter Chromium ab. Der häufigste Fall sind Suites, die dieses Flag vor Jahren wegen eines ganz anderen Problems ergänzt haben. Suchen Sie danach, bevor Sie eine Zeile Löser-Code schreiben.

# Run in Chrome, which uses native automation by default.
npx testcafe chrome tests/checkout.js

# This flag puts you back on the proxy and breaks reCAPTCHA.
# npx testcafe chrome tests/checkout.js --disable-native-automation

Eines sei klar gesagt, weil TestCafe es ebenfalls sagt: Wenn Ihnen die getestete Website gehört, besteht die beste Antwort darin, gar nichts zu lösen. Google veröffentlicht einen v2-Test-Sitekey, der immer durchgeht, und einen separaten v3-Key mit gelockertem Schwellenwert einzurichten, ist in der reCAPTCHA-Konsole eine Änderung von fünf Minuten. Das eigene reCAPTCHA-Rezept von TestCafe geht auf beides ein. Lösen ist für die Fälle gedacht, in denen diese Tür verschlossen ist: ein Checkout eines Drittanbieters mitten im Ablauf, eine Staging-Umgebung, die sich die Produktions-Keys teilt, oder ein Smoke-Test, der gegen die echte Website laufen muss.

Schritt 1: Den Sitekey aus der Seite auslesen

Selektoren in TestCafe sind lazy und wiederholen ihren Versuch, ein Selector, der geschrieben wurde, bevor das Widget rendert, löst sich also trotzdem auf, sobald es erscheint. Holen Sie den Sitekey vom Widget-Element, statt ein Literal in den Test zu kopieren, dann übersteht derselbe Test auch eine Rotation des Keys.

// npm install capskip
import { Selector } from 'testcafe';

const PAGE_URL = 'https://example.com/page-with-recaptcha';

fixture('Checkout').page(PAGE_URL);

test('submits behind reCAPTCHA', async t => {
    const widget = Selector('.g-recaptcha');
    const sitekey = await widget.getAttribute('data-sitekey');
});

Schritt 2: Aus der Testdatei heraus lösen

Hier ist TestCafe einfacher als die Runner, die im Browser laufen. Ihre Testfunktion ist ganz normales Node, das SDK ist also ein schlichter Import und der Aufruf ein schlichtes await. Es gibt keine Brücke zu bauen und keinen Task zu registrieren, also genau den Teil, den viele nach dem Umstieg von Cypress erwarten.

// npm install capskip
import { CapSkip } from 'capskip';

// Local mode. Change only the host to talk to a solver
// running on another machine.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

const result = await solver.recaptcha(sitekey, PAGE_URL);
const token = result.code;

Diese eine Methode deckt reCAPTCHA v2, Invisible, Enterprise und v3 ab. Die Varianten sind Optionen im dritten Argument statt eigener Aufrufe: invisible auf 1, enterprise auf 1 oder version auf v3 mit einem Action-String. Turnstile und GeeTest haben eigene Methoden mit demselben Aufbau. Alle Parameter finden Sie in der CapSkip-API-Dokumentation.

Schritt 3: Das Token mit einer ClientFunction hineinschreiben

Das Response-Feld ist eine versteckte Textarea, die normale Tippaktion fasst es also nicht an. TestCafe-Aktionen wirken nur auf sichtbare Elemente, und das ist Absicht. Eine ClientFunction führt Ihren Code stattdessen innerhalb der Seite aus, und das ist das richtige Werkzeug für ein Feld, in das ein echter Nutzer nie etwas tippt.

In diese Falle tappt hier fast jeder einmal. Eine ClientFunction sieht keine Variablen aus dem umgebenden Test. Der Funktionsrumpf wird serialisiert und an den Browser geschickt, ein Token aus dem umschließenden Scope kommt dort zur Laufzeit also als undefinierter Bezeichner an. Übergeben Sie es als Argument oder als deklarierte Abhängigkeit.

// The token is a parameter, not a closure variable.
import { ClientFunction } from 'testcafe';

const injectToken = ClientFunction(value => {
    const field = document.getElementById('g-recaptcha-response');
    field.value = value;
    field.dispatchEvent(new Event('change', { bubbles: true }));
});

await injectToken(token);

Die Empfehlung von TestCafe lautet, Client Functions nicht dazu zu benutzen, das Verhalten einer Website dauerhaft zu verändern, und an diese Empfehlung sollte man sich halten. Einen einzigen Wert für einen einzigen Lauf in ein einziges Formularfeld zu schreiben, ist etwas anderes. Sie füllen ein Feld aus, statt das Verhalten der Seite zu patchen, und der Wert ist weg, sobald der Lauf endet.

Manche Formulare warten auf einen Callback, statt die Textarea auszulesen. Wenn das Widget ein data-callback-Attribut deklariert, rufen Sie diese Funktion in derselben ClientFunction mit dem Token auf, und die Seite macht genau so weiter wie bei einem Menschen.

Vollständiges lauffähiges Beispiel

Der komplette Test. Sitekey auslesen, lösen, injizieren, absenden, prüfen.

// npm install capskip
import { Selector, ClientFunction } from 'testcafe';
import { CapSkip, NetworkException } from 'capskip';

const PAGE_URL = 'https://example.com/page-with-recaptcha';
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

const injectToken = ClientFunction(value => {
    const field = document.getElementById('g-recaptcha-response');
    field.value = value;
    field.dispatchEvent(new Event('change', { bubbles: true }));
});

fixture('Checkout').page(PAGE_URL);

test('submits the protected form', async t => {
    const sitekey = await Selector('.g-recaptcha').getAttribute('data-sitekey');

    let token;
    try {
        token = (await solver.recaptcha(sitekey, PAGE_URL)).code;
    } catch (err) {
        if (err instanceof NetworkException) {
            throw new Error('CapSkip is not reachable on 127.0.0.1:8080.');
        }
        throw err;
    }

    await injectToken(token);
    await t.click(Selector('button[type=submit]'));
    await t.expect(Selector('.thank-you').exists).ok();
});

Lösen Sie so spät wie möglich. Ein Token ist nur einmal verwendbar und läuft nach etwa zwei Minuten ab, ein Token, das in einem Fixture-Hook gelöst wurde, der vor drei weiteren Tests läuft, ist also tot, wenn der vierte Test es abschickt. Setzen Sie den Aufruf in den Test, der ihn braucht.

Timeouts, und das eine, das wirklich zuschlägt

Ein reCAPTCHA-Lösen dauert Dutzende Sekunden, also länger als mehrere Standardwerte von TestCafe. Die gute Nachricht: Die Timeouts, an die man zuerst denkt, sind gar nicht beteiligt. Ihr Löser-Aufruf ist ein await innerhalb der Testfunktion und keine Seitenaktion, das Selector-Timeout von 10 Sekunden und das Assertion-Timeout von 3 Sekunden bekommen ihn also nie zu sehen.

Die Grenze, auf die es ankommt, ist das Timeout für die Testausführung, das begrenzt, wie lange ein einzelner Test laufen darf. Es hat keinen Standardwert und schlägt daher erst zu, wenn jemand es setzt. Wenn Ihre CI-Konfiguration ein Timeout für die Testausführung übergibt, achten Sie darauf, dass der Wert zusätzlich zu allem anderen, was der Test tut, Raum für ein langsames Lösen lässt. Auch das SDK hat seine eigene Obergrenze: recaptchaTimeout steht standardmäßig auf 300 Sekunden und wirft eine TimeoutException, wenn ein Lösen sie überschreitet.

Den Löser woanders betreiben

Tests wandern in die CI, und ein CI-Runner ist nicht Ihr eigener Rechner. Am Code oben ändert sich nichts außer dem Host-String.

CapSkip hat zwei Verbindungsmodi. Local bindet an 127.0.0.1 und antwortet nur diesem einen Gerät, und genau das wollen Sie, während Sie den Test schreiben. Server bindet an Ihre Netzwerk-IP oder öffentliche IP, sodass ein Build-Agent, ein Container oder eine VM dieselbe Windows-Maschine über die API anspricht. Eine statische öffentliche IP hält diese Adresse stabil. Es ist Ihre Hardware und sie wird in beiden Modi nicht nach Verbrauch abgerechnet, eine Suite, die pro Nacht fünfhundert Captchas löst, kostet also genau so viel wie eine, die fünf löst.

// Same SDK, same call. Only the host moves.
const solver = new CapSkip({
    host: process.env.CAPSKIP_HOST || '127.0.0.1',
    port: 8080,
    apiKey: process.env.CAPSKIP_API_KEY,
});

Das SDK liest CAPSKIP_HOST, CAPSKIP_PORT und CAPSKIP_API_KEY von sich aus aus der Umgebung, ein CI-Job kann dieselbe Testdatei also mit zwei Variablen und ohne Codeänderung auf einen entfernten Löser richten. Aktivieren Sie die Key-Validierung, sobald der Löser auf einer Netzwerkadresse lauscht, und geben Sie jedem Runner einen eigenen Key, damit sich einer widerrufen lässt, ohne die anderen anzufassen. Beide Modi werden Schritt für Schritt erklärt unter CapSkip-Einrichtungsanleitung.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Der Worker lässt sich nicht erstellen: Das Skript ist vom Origin aus nicht zugänglichDer hammerhead-Proxy liegt im Pfad, der Origin der Seite ist also nicht die WebsiteDas Flag disable-native-automation entfernen und in Chrome oder Edge laufen lassen
Jeder v3-Score kommt als 0,1 zurückGleiche Ursache. Der Proxy nagelt den Score fest, egal was der Test tutGleiche Lösung. Die native Automatisierung entfernt den Proxy vollständig
ReferenceError, wonach das Token nicht definiert istDer Rumpf der ClientFunction kann keine Variablen aus dem äußeren Scope lesenDas Token als Argument oder als deklarierte Abhängigkeit übergeben
Die Tippaktion schlägt beim Response-Feld fehlDie Textarea ist versteckt, und Aktionen brauchen ein sichtbares ElementDen Wert stattdessen in einer ClientFunction setzen
Das Formular weist ein Token ab, das in Ordnung aussiehtEs wurde in einem Hook gelöst, Minuten vor dem AbsendenIm Test selbst lösen, unmittelbar vor dem Absenden
NetworkExceptionCapSkip läuft nicht, oder der Host ist falschDie App starten oder host auf die Serveradresse zeigen lassen
TimeoutExceptionDie Lösung hat recaptchaTimeout überdauertErhöhen Sie ihn über den Standardwert von 300 Sekunden
ValidationExceptionFehlender oder fehlerhafter Sitekey oder fehlerhafte Seiten-URLBeide vor dem Aufruf loggen und prüfen, ob der Sitekey der aktive ist

FAQ

Brauche ich einen Task oder ein Plugin, so wie bei Cypress?

Nein. Cypress führt Ihren Testcode im Browser aus, alles, was Node braucht, muss also eine Brücke überqueren. TestCafe führt Ihren Testcode von Anfang an in Node aus, und nur die Rümpfe der ClientFunctions gehen in den Browser, der Löser-Aufruf ist also ein ganz normaler Import. Die Cypress-Variante dieser Aufgabe ist beschrieben in der Cypress-Captcha-Anleitung.

Geht das auch in Firefox oder Safari?

Sie können den Test laufen lassen, aber rechnen Sie damit, dass sich das Widget selbst danebenbenimmt, denn TestCafe fällt bei diesen Browsern auf den Proxy zurück, und das ist die Konfiguration, die reCAPTCHA nicht übersteht. Halten Sie die Tests mit Captcha auf Chrome oder Edge und lassen Sie die Cross-Browser-Matrix die Seiten abdecken, auf denen kein Widget sitzt.

Funktioniert das auch für Turnstile?

Ja, mit zwei Unterschieden. Die Methode heißt turnstile statt recaptcha, und das zu füllende Feld ist das versteckte Input namens cf-turnstile-response. Eine vollständige Challenge-Seite braucht zusätzlich die Werte data und pagedata sowie den User-Agent, der mit dem Token zurückkommt. Das alles finden Sie auf die Cloudflare-Turnstile-Löser-Seite.

Sollen Captcha-Tests bei jedem Commit laufen?

Meistens nicht, und der Grund ist die Geschwindigkeit, nicht der Preis. Das Lösen wird hier nicht nach Verbrauch abgerechnet, aber Dutzende Sekunden pro Test sind eine langsame Prüfung für einen Pull Request. Markieren Sie sie mit einem Tag und lassen Sie sie in einem nächtlichen Job oder vor einem Release laufen, und halten Sie die schnelle Suite auf einen Build gerichtet, der Test-Keys verwendet.

Die Kurzfassung

Bestätigen Sie, dass die native Automatisierung aktiv ist, denn der alte Proxy macht reCAPTCHA schon für sich allein kaputt. Lesen Sie den Sitekey mit einem Selector, rufen Sie den Löser direkt aus der Testdatei auf, da diese ohnehin Node ist, und schreiben Sie das Token über eine ClientFunction hinein, wobei Sie den Wert als Argument übergeben. Lösen Sie unmittelbar bevor Sie absenden.

Den Rest der Node.js-Oberfläche finden Sie auf die Node.js-Captcha-Solver-Seite. Alles, was speziell für diesen Captcha-Typ gilt, finden Sie auf die reCAPTCHA-v2-Solver-Seite. Dieselbe Aufgabe in einem WebDriver-basierten Runner steht in der WebdriverIO-Anleitung.

Noch eine letzte Sache, bevor Sie das in die CI einbauen. CapSkip ist ein lokaler Captcha-Löser der auf Hardware läuft, die Ihnen bereits gehört, sodass eine nächtliche Suite, die tausend Captchas löst, genau so viel kostet wie eine, die zehn löst.