Use Cases

Solve CAPTCHA untuk Pengujian Endpoint API pada Form Web

Form yang dijaga reCAPTCHA atau Turnstile sulit diuji di pipeline CI: setiap kali test berjalan, ada tantangan CAPTCHA yang menunggu jawaban manusia. Jalan keluarnya bukan mematikan proteksi di staging, melainkan menyelesaikan CAPTCHA lewat API, mengambil token-nya, lalu mem-POST token itu langsung ke endpoint backend — tanpa browser sama sekali. Dengan begitu satu suite pengujian bisa memverifikasi bahwa server menerima token valid dan menolak token palsu secara otomatis di setiap commit.


Kapan pendekatan ini Anda perlukan

Menyuntik token CAPTCHA langsung ke endpoint berguna dalam empat situasi yang sering muncul di tim QA dan data:

  • Validasi backend: memastikan server benar-benar memverifikasi token CAPTCHA ke penyedia, bukan sekadar menerima field apa pun yang dikirim.
  • Load testing: mengirim banyak permintaan ke endpoint yang dilindungi CAPTCHA untuk mengukur perilaku di bawah beban.
  • Pengujian integrasi: menjalankan pengujian API pengiriman form sebagai bagian dari alur CI/CD.
  • Pengujian respons kesalahan: memastikan endpoint mengembalikan kode error yang tepat untuk token yang invalid atau kedaluwarsa.

Kenapa uji langsung ke endpoint, bukan lewat browser

Menggerakkan browser headless hanya untuk melewati satu widget CAPTCHA itu lambat, rapuh, dan boros di runner CI. Pendekatan berbasis API memotong seluruh lapisan UI: Anda memesan token dari CaptchaAI lewat HTTP biasa, menyusun payload form, lalu menembak endpoint apa adanya. Hasilnya deterministik dan cepat — cocok untuk job GitHub Actions yang harus tuntas dalam hitungan detik, bukan menit.

Pendekatan ini juga netral terhadap bahasa dan infrastruktur: klien HTTP apa pun bisa dipakai, dan endpoint yang Anda uji boleh berjalan di mana saja — misalnya service yang di-deploy ke AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta). Satu catatan penting: uji hanya endpoint milik Anda sendiri atau yang Anda punya izin resmi untuk mengujinya, sejalan dengan UU Pelindungan Data Pribadi (UU 27/2022).


Alur kerja: solve, susun payload, POST, validasi

Setiap kasus uji mengikuti empat langkah yang sama:

┌──────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Solve    │────▶│ Build      │────▶│ POST to      │────▶│ Validate     │
│ CAPTCHA  │     │ Request    │     │ Endpoint     │     │ Response     │
│ (API)    │     │ Payload    │     │              │     │              │
└──────────┘     └────────────┘     └──────────────┘     └──────────────┘

Tidak diperlukan browser untuk sebagian besar pengujian endpoint.

Langkah pertama menyelesaikan CAPTCHA dan mengembalikan token; langkah kedua menaruh token itu ke field yang diharapkan backend (g-recaptcha-response untuk reCAPTCHA, cf-turnstile-response untuk Turnstile); langkah ketiga mengirim payload ke endpoint; langkah terakhir memeriksa kode status dan isi respons.


Menyiapkan TokenProvider

Kelas TokenProvider membungkus pola empat langkah CaptchaAI — kirim task ke in.php, simpan task ID, polling res.php, lalu pakai token. Ia menangani reCAPTCHA v2, reCAPTCHA v3, dan Turnstile lewat satu antarmuka:

import time
import requests

class TokenProvider:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    def get_recaptcha_token(self, sitekey, pageurl, version="v2"):
        params = {
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
        }
        if version == "v3":
            params["version"] = "v3"
            params["action"] = "submit"
        return self._solve(params, initial_wait=15 if version == "v3" else 10)

    def get_turnstile_token(self, sitekey, pageurl):
        return self._solve({
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": pageurl,
        })

    def _solve(self, params, initial_wait=10):
        params["key"] = self.api_key
        params["json"] = 1
        resp = requests.post(f"{self.BASE}/in.php", data=params).json()
        if resp["status"] != 1:
            raise Exception(resp["request"])
        task_id = resp["request"]
        time.sleep(initial_wait)
        for _ in range(60):
            result = requests.get(
                f"{self.BASE}/res.php",
                params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
            ).json()
            if result["request"] == "CAPCHA_NOT_READY":
                time.sleep(5)
                continue
            if result["status"] == 1:
                return result["request"]
            raise Exception(result["request"])
        raise TimeoutError("Timed out")

Perhatikan initial_wait yang berbeda per tipe: reCAPTCHA v3 diberi jeda awal lebih panjang karena butuh waktu lebih lama untuk diselesaikan, sedangkan v2 dan Turnstile umumnya lebih cepat.

Menguji token valid, invalid, dan hilang

EndpointTester menjalankan tiga pemeriksaan pada tiap endpoint. test_endpoint mengirim token asli dan mengharapkan respons sukses; test_invalid_token menyuntik token palsu (INVALID_TOKEN_12345) dan mengharapkan penolakan; test_missing_token mengirim permintaan tanpa field CAPTCHA sama sekali. Dua pengujian terakhir adalah inti audit keamanan: kalau endpoint menerima token palsu atau permintaan tanpa token, validasi CAPTCHA di server Anda bocor. run_suite menjalankan ketiga pemeriksaan untuk setiap konfigurasi, dan report merangkum hasilnya menjadi laporan lolos/gagal yang mudah dibaca di log CI.

import json
import time

class EndpointTester:
    def __init__(self, api_key):
        self.token_provider = TokenProvider(api_key)
        self.session = requests.Session()
        self.results = []

    def test_endpoint(self, config):
        """
        config: {
            "name": "test name",
            "url": "endpoint URL",
            "method": "POST",
            "captcha_type": "recaptcha_v2" | "recaptcha_v3" | "turnstile",
            "sitekey": "...",
            "pageurl": "...",
            "captcha_field": "g-recaptcha-response",
            "payload": { ... form data ... },
            "expected_status": 200,
            "expected_contains": "success",
        }
        """
        start = time.time()
        result = {"name": config["name"], "passed": False}

        try:
            # Get CAPTCHA token
            captcha_type = config.get("captcha_type", "recaptcha_v2")
            if captcha_type == "recaptcha_v2":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"]
                )
            elif captcha_type == "recaptcha_v3":
                token = self.token_provider.get_recaptcha_token(
                    config["sitekey"], config["pageurl"], version="v3"
                )
            elif captcha_type == "turnstile":
                token = self.token_provider.get_turnstile_token(
                    config["sitekey"], config["pageurl"]
                )
            else:
                raise ValueError(f"Unknown captcha type: {captcha_type}")

            # Build payload
            payload = {**config.get("payload", {})}
            captcha_field = config.get("captcha_field", "g-recaptcha-response")
            payload[captcha_field] = token

            # Submit request
            method = config.get("method", "POST").upper()
            headers = config.get("headers", {})

            if config.get("json_body"):
                resp = self.session.request(
                    method, config["url"], json=payload, headers=headers
                )
            else:
                resp = self.session.request(
                    method, config["url"], data=payload, headers=headers
                )

            # Validate response
            result["status_code"] = resp.status_code
            result["response_length"] = len(resp.text)
            result["elapsed"] = round(time.time() - start, 2)

            # Check expected status
            expected_status = config.get("expected_status", 200)
            if resp.status_code != expected_status:
                result["error"] = f"Expected {expected_status}, got {resp.status_code}"
                self.results.append(result)
                return result

            # Check expected content
            expected = config.get("expected_contains")
            if expected and expected.lower() not in resp.text.lower():
                result["error"] = f"Response missing: '{expected}'"
                self.results.append(result)
                return result

            result["passed"] = True

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_invalid_token(self, config):
        """Test that endpoint rejects invalid CAPTCHA tokens."""
        invalid_config = {**config}
        invalid_config["name"] = f"{config['name']} (invalid token)"

        # Override with fake token
        payload = {**config.get("payload", {})}
        captcha_field = config.get("captcha_field", "g-recaptcha-response")
        payload[captcha_field] = "INVALID_TOKEN_12345"

        start = time.time()
        result = {"name": invalid_config["name"], "passed": False}

        try:
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            # Should reject — 4xx or error message
            if resp.status_code >= 400 or "error" in resp.text.lower() or "invalid" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted invalid CAPTCHA token"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def test_missing_token(self, config):
        """Test that endpoint rejects missing CAPTCHA token."""
        start = time.time()
        result = {"name": f"{config['name']} (missing token)", "passed": False}

        try:
            payload = config.get("payload", {})
            resp = self.session.post(config["url"], data=payload)
            result["status_code"] = resp.status_code
            result["elapsed"] = round(time.time() - start, 2)

            if resp.status_code >= 400 or "captcha" in resp.text.lower():
                result["passed"] = True
            else:
                result["error"] = "Endpoint accepted request without CAPTCHA"

        except Exception as e:
            result["error"] = str(e)
            result["elapsed"] = round(time.time() - start, 2)

        self.results.append(result)
        return result

    def run_suite(self, configs):
        """Run a full test suite against multiple endpoints."""
        for config in configs:
            self.test_endpoint(config)
            self.test_invalid_token(config)
            self.test_missing_token(config)
        return self.report()

    def report(self):
        passed = sum(1 for r in self.results if r["passed"])
        total = len(self.results)
        lines = [f"Endpoint Tests: {passed}/{total} passed", "=" * 50]
        for r in self.results:
            status = "PASS" if r["passed"] else "FAIL"
            elapsed = r.get("elapsed", "?")
            lines.append(f"  [{status}] {r['name']} ({elapsed}s)")
            if r.get("error"):
                lines.append(f"         Error: {r['error']}")
        return "\n".join(lines)

Menjalankan suite terhadap endpoint staging

Definisikan daftar konfigurasi — satu per endpoint — lalu jalankan seluruh suite. Contoh berikut menguji dua endpoint sekaligus: form kontak yang memakai reCAPTCHA v2 dan pendaftaran newsletter yang memakai Turnstile.

tester = EndpointTester("YOUR_API_KEY")

configs = [
    {
        "name": "Contact form submission",
        "url": "https://example.com/api/contact",
        "captcha_type": "recaptcha_v2",
        "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        "pageurl": "https://example.com/contact",
        "captcha_field": "g-recaptcha-response",
        "payload": {
            "name": "Test User",
            "email": "test@example.com",
            "message": "Automated test message",
        },
        "expected_status": 200,
        "expected_contains": "success",
    },
    {
        "name": "Newsletter signup",
        "url": "https://example.com/api/subscribe",
        "captcha_type": "turnstile",
        "sitekey": "0x4AAAA...",
        "pageurl": "https://example.com/newsletter",
        "captcha_field": "cf-turnstile-response",
        "payload": {
            "email": "test@example.com",
        },
        "expected_status": 200,
    },
]

report = tester.run_suite(configs)
print(report)

Keluarannya menampilkan setiap kasus uji beserta waktunya:

Endpoint Tests: 5/6 passed
==================================================
  [PASS] Contact form submission (18.5s)
  [PASS] Contact form submission (invalid token) (0.3s)
  [PASS] Contact form submission (missing token) (0.2s)
  [PASS] Newsletter signup (14.2s)
  [FAIL] Newsletter signup (invalid token) (0.3s)
         Error: Endpoint accepted invalid CAPTCHA token
  [PASS] Newsletter signup (missing token) (0.2s)

Baris yang gagal justru temuan berharga: "Newsletter signup (invalid token)" gagal karena endpoint menerima token palsu — persis jenis bug validasi yang ingin Anda tangkap sebelum rilis ke produksi.


Membaca kegagalan dan memperbaikinya

Sebagian kegagalan berasal dari konfigurasi pengujian, bukan dari bug endpoint. Tabel ini memetakan gejala yang paling sering muncul:

Gejala Penyebab Solusi
Token valid ditolak Token kedaluwarsa sebelum dikirim Perkecil jeda antara solve dan pengiriman
Token invalid diterima Backend tidak memvalidasi CAPTCHA Bug — ini celah keamanan yang perlu dilaporkan
403 pada semua request Token CSRF atau cookie sesi tidak ada Tambahkan cookie sesi atau header CSRF
Endpoint JSON menolak form data Content-type keliru Set json_body: True di konfigurasi

Berapa thread yang dipakai pengujian ini

CaptchaAI menagih per thread bersamaan, bukan per solve, dan setiap paket memberi solve tanpa batas selama sebulan. Untuk suite pengujian, biaya Anda ditentukan oleh berapa banyak token yang diminta secara paralel, bukan total jumlah pengujian. Suite yang berjalan berurutan seperti contoh di atas hanya memakai satu token dalam satu waktu, sehingga paket BASIC ($15/bulan, 5 thread) sudah memadai. Untuk load testing dengan puluhan permintaan paralel, naik ke ADVANCE ($90/bulan, 50 thread) memberi ruang lebih. Karena tarif tetap per bulan, biaya pengujian bisa diprediksi berapa pun kali suite Anda dijalankan di CI. Harga terkini ada di captchaai.com/pricing.


Pertanyaan umum

Apakah saya perlu token asli untuk menguji penolakan token palsu?

Tidak. Pengujian token invalid dan token hilang tidak memerlukan penyelesaian CAPTCHA — cukup kirim permintaan dengan token palsu atau tanpa field CAPTCHA. Token asli dari CaptchaAI hanya Anda perlukan untuk menguji jalur pengiriman yang sukses.

Berapa lama waktu penyelesaian token saat pengujian berjalan?

Bergantung tipe CAPTCHA. Pada contoh di atas, kasus reCAPTCHA v2 selesai sekitar 18 detik dan Turnstile sekitar 14 detik termasuk waktu round-trip ke endpoint, sedangkan pemeriksaan token invalid dan hilang selesai di bawah satu detik karena tidak memanggil solver.

Bagaimana kalau endpoint saya justru memakai hCaptcha?

Sayangnya belum bisa. CaptchaAI tidak mendukung hCaptcha maupun FunCaptcha saat ini, jadi untuk endpoint yang dilindungi keduanya Anda memerlukan layanan lain. Untuk reCAPTCHA v2/v3, Turnstile, dan GeeTest v3, pola di artikel ini berlaku langsung.

Ambil dulu halaman form memakai session yang sama, lalu sertakan cookie dan header CSRF pada permintaan berikutnya. Objek requests.Session di EndpointTester sudah menyimpan cookie antar permintaan secara otomatis.

Bisakah suite ini berjalan otomatis di pipeline CI/CD?

Bisa. Jalankan run_suite sebagai satu langkah pengujian dan kaitkan kode keluar dengan jumlah kasus yang lolos, sehingga build gagal begitu validasi CAPTCHA di backend bermasalah.


Panduan terkait


Uji setiap endpoint yang dilindungi CAPTCHA — pakai CaptchaAI.

Komentar dinonaktifkan untuk artikel ini.