Use Cases

Penanganan CAPTCHA untuk Pendaftaran Mata Kuliah Universitas

Suite pengujian portal KRS Anda tidak perlu dimatikan hanya karena reCAPTCHA v2 muncul di halaman login: ambil sitekey dari HTML, kirim ke API CaptchaAI, tunggu token, lalu tempelkan token itu ke field g-recaptcha-response pada form yang sedang diuji. Empat langkah itu mengubah skenario yang selama ini dikerjakan manual — satu orang QA duduk mengklik gambar sebelum setiap run — menjadi pengujian end-to-end yang bisa dijalankan di CI. Artikel ini menunjukkan pola tersebut untuk alur khas sistem akademik: login mahasiswa, pencarian mata kuliah, pengambilan KRS, kuota kursi, dan daftar tunggu.

Kenapa masa KRS adalah kasus uji paling berat di kampus

Beban puncak sistem akademik Indonesia sangat terkonsentrasi: jendela pengisian KRS hanya terbuka beberapa hari per semester, dan ribuan mahasiswa masuk ke SIAKAD dalam rentang jam yang sama. Justru di situ perlindungan anti-bot bekerja paling agresif, karena login berulang, pencarian kelas beruntun, dan pengambilan kursi bersamaan adalah pola yang persis ditargetkan reCAPTCHA v2.

Akibatnya, skenario yang paling perlu diuji sebelum KRS dibuka — uji beban ratusan sesi mahasiswa simulasi, perebutan kursi terakhir, perilaku daftar tunggu saat kuota nol — justru yang paling sering terhalang tantangan. Menonaktifkan CAPTCHA di staging memang paling bersih, tetapi tim yang perlu mereproduksi perilaku produksi tetap butuh cara menyelesaikannya secara terprogram.

Satu catatan kepatuhan: pola ini untuk portal milik institusi Anda sendiri atau sistem yang Anda punya izin uji tertulis. Data mahasiswa termasuk data pribadi menurut UU 27/2022 (UU PDP), jadi pakai akun uji dan dataset sintetis.

Titik-titik CAPTCHA yang perlu dipetakan sebelum menulis tes

Petakan dulu di langkah mana saja tantangan muncul. Peta ini menentukan berapa banyak solve yang dibutuhkan satu run penuh — dan karena CaptchaAI menagih per thread, bukan per solve, angka itu memengaruhi pilihan paket, bukan biaya per skenario.

Langkah pendaftaran Jenis CAPTCHA Tujuan proteksi
Halaman login mahasiswa reCAPTCHA v2 Cegah percobaan kredensial otomatis
Pencarian / telusur mata kuliah reCAPTCHA v2 Batasi kueri otomatis
Ambil / batal mata kuliah reCAPTCHA v2 Cegah pengisian KRS otomatis
Pendaftaran daftar tunggu reCAPTCHA v2 Blokir bot pendaftaran
Perencana jadwal Cloudflare Turnstile Lindungi alat interaktif
Permintaan transkrip reCAPTCHA v2 Verifikasi pengguna manusia

Kedua keluarga tantangan itu ditangani CaptchaAI: reCAPTCHA v2 lewat method userrecaptcha, Cloudflare Turnstile lewat method turnstile. Alurnya sama — kirim ke in.php, simpan task ID, polling res.php, pakai token — jadi satu helper melayani keduanya cukup dengan mengganti parameter method.

Kelas penguji Python untuk alur KRS

Kelas berikut membungkus keempat skenario inti dalam satu session requests, sehingga cookie login bertahan antar-tes. _solve_recaptcha dipanggil hanya ketika _has_captcha menemukan penanda reCAPTCHA — pengecekan bersyarat ini menghemat solve pada halaman yang kebetulan bersih.

import requests
import time
import re

class RegistrationTester:
    def __init__(self, api_key, portal_url):
        self.api_key = api_key
        self.portal_url = portal_url
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def test_login(self, student_id, password):
        """Test login flow with CAPTCHA handling."""
        login_page = self.session.get(f"{self.portal_url}/login")

        site_key = self._extract_site_key(login_page.text)
        token = self._solve_recaptcha(site_key, f"{self.portal_url}/login")

        response = self.session.post(f"{self.portal_url}/login", data={
            "studentId": student_id,
            "password": password,
            "g-recaptcha-response": token
        })

        return {
            "status": response.status_code,
            "redirected": response.url != f"{self.portal_url}/login",
            "success": "dashboard" in response.url or response.status_code == 200
        }

    def test_course_search(self, term, department, level=None):
        """Test course search returns correct results."""
        params = {"term": term, "dept": department}
        if level:
            params["level"] = level

        response = self.session.get(
            f"{self.portal_url}/courses/search", params=params
        )

        if self._has_captcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(
                site_key, f"{self.portal_url}/courses/search"
            )
            response = self.session.post(
                f"{self.portal_url}/courses/search",
                data={**params, "g-recaptcha-response": token}
            )

        courses = self._parse_courses(response.text)
        return {
            "count": len(courses),
            "courses": courses,
            "all_match_dept": all(
                c["department"] == department for c in courses
            )
        }

    def test_enrollment(self, course_id, section):
        """Test enrolling in a course section."""
        enroll_page = self.session.get(
            f"{self.portal_url}/enroll/{course_id}/{section}"
        )

        if self._has_captcha(enroll_page.text):
            site_key = self._extract_site_key(enroll_page.text)
            token = self._solve_recaptcha(
                site_key,
                f"{self.portal_url}/enroll/{course_id}/{section}"
            )
        else:
            token = None

        form_data = {
            "courseId": course_id,
            "section": section,
            "confirm": "true"
        }
        if token:
            form_data["g-recaptcha-response"] = token

        response = self.session.post(
            f"{self.portal_url}/enroll/submit",
            data=form_data
        )

        return {
            "status": response.status_code,
            "enrolled": "success" in response.text.lower(),
            "waitlisted": "waitlist" in response.text.lower(),
            "error": self._extract_error(response.text)
        }

    def test_seat_availability(self, course_ids):
        """Test seat availability display for multiple courses."""
        results = []
        for course_id in course_ids:
            response = self.session.get(
                f"{self.portal_url}/courses/{course_id}"
            )

            if self._has_captcha(response.text):
                site_key = self._extract_site_key(response.text)
                token = self._solve_recaptcha(
                    site_key, f"{self.portal_url}/courses/{course_id}"
                )
                response = self.session.post(
                    f"{self.portal_url}/courses/{course_id}",
                    data={"g-recaptcha-response": token}
                )

            availability = self._parse_availability(response.text)
            results.append({"course_id": course_id, **availability})

        return results

    def _has_captcha(self, html):
        return "g-recaptcha" in html or "recaptcha" in html.lower()

    def _extract_site_key(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            return match.group(1)
        raise ValueError("reCAPTCHA site key not found")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": page_url,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(60):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("reCAPTCHA solve timed out")

    def _parse_courses(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_or_empty(node):
            return node.text.strip() if node and node.text else ""

        courses = []
        for row in soup.select(".course-row, tr.course"):
            courses.append({
                "id": row.get("data-course-id", ""),
                "department": text_or_empty(row.select_one(".dept")),
                "number": text_or_empty(row.select_one(".number")),
                "title": text_or_empty(row.select_one(".title")),
                "seats": text_or_empty(row.select_one(".seats"))
            })
        return courses

    def _parse_availability(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        return {
            "total_seats": text_or_none(soup.select_one(".total-seats")),
            "enrolled": text_or_none(soup.select_one(".enrolled-count")),
            "available": text_or_none(soup.select_one(".available-seats")),
            "waitlist": text_or_none(soup.select_one(".waitlist-count"))
        }

    def _extract_error(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        error = soup.select_one(".error-message, .alert-danger")
        return error.text.strip() if error else None

Tiga detail sering terlewat saat mengadaptasinya ke SIAKAD tertentu. Pertama, _extract_site_key mengandalkan atribut data-sitekey pada HTML yang dirender server; portal SPA merender widget belakangan sehingga sitekey belum ada di respons awal. Kedua, pageurl harus persis sama dengan URL halaman tempat widget tampil — salah URL, token ditolak portal. Ketiga, loop polling berhenti setelah 60 percobaan berjeda 3 detik; untuk suite besar, pakai jeda yang meningkat eksponensial (exponential backoff).

Menjalankan seluruh skenario sebagai satu suite (JavaScript)

Di stack Node.js, pola yang sama bisa dijalankan sebagai runner yang mengumpulkan hasil tiap skenario dan mencetak laporan ringkas — berguna sebagai artefak build di pipeline CI.

class RegistrationTestSuite {
  constructor(apiKey, portalUrl) {
    this.apiKey = apiKey;
    this.portalUrl = portalUrl;
    this.results = [];
  }

  async runAll(testCredentials) {
    const tests = [
      () => this.testLogin(testCredentials),
      () => this.testCourseSearch('Fall2025', 'CS'),
      () => this.testEnrollment('CS101', '001'),
      () => this.testDropCourse('CS101', '001'),
      () => this.testWaitlistFlow('CS201', '001')
    ];

    for (const test of tests) {
      try {
        const result = await test();
        this.results.push({ ...result, passed: true });
      } catch (error) {
        this.results.push({ name: test.name, error: error.message, passed: false });
      }
    }

    return this.generateReport();
  }

  async solveRecaptcha(pageUrl, html) {
    const match = html.match(/data-sitekey="([^"]+)"/);
    if (!match) return null;

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'userrecaptcha',
        googlekey: match[1],
        pageurl: pageUrl,
        json: '1'
      })
    });
    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) return data.request;
    }
    throw new Error('reCAPTCHA solve timed out');
  }

  generateReport() {
    const passed = this.results.filter(r => r.passed).length;
    const failed = this.results.filter(r => !r.passed).length;

    return {
      total: this.results.length,
      passed,
      failed,
      passRate: `${((passed / this.results.length) * 100).toFixed(1)}%`,
      details: this.results
    };
  }
}

// Usage
const suite = new RegistrationTestSuite('YOUR_API_KEY', 'https://register.university.edu');
const report = await suite.runAll({ studentId: 'test123', password: 'testpass' });
console.log(`Tests: ${report.passed}/${report.total} passed (${report.passRate})`);

Runner ini menangkap kesalahan per skenario alih-alih menghentikan seluruh run, sehingga satu tes daftar tunggu yang gagal tidak menyembunyikan hasil tes login. Untuk perilaku token pada widget yang memakai fungsi callback, lihat cara menangani callback reCAPTCHA v2 lewat API dan penjelasan mekanisme callback reCAPTCHA v2.

Skenario mana yang paling banyak memakan solve

Tidak semua tes memakan anggaran solve yang sama. Skenario di baris atas menghabiskan solve jauh lebih cepat daripada baris bawah.

Skenario pengujian Frekuensi CAPTCHA Dampak pada suite
Login banyak akun mahasiswa uji Tinggi Hampir setiap percobaan login memunculkan tantangan
Pengambilan KRS bersamaan Tinggi Proteksi anti-bot aktif penuh
Pencarian mata kuliah beruntun Sedang Kueri terkena pembatasan laju permintaan (rate limiting)
Pembuatan jadwal Rendah Interaksi dalam satu sesi
Validasi prasyarat Rendah Bagian dari alur pengambilan KRS

Karena paket CaptchaAI dihitung per thread bersamaan dengan solve tak terbatas per thread, yang perlu diperkirakan adalah jumlah solve yang berjalan serentak, bukan total sebulan. Suite QA satu tim biasanya cukup di BASIC ($15/bulan, 5 thread) atau STANDARD ($30/bulan, 15 thread); uji beban ratusan sesi mahasiswa sekaligus baru masuk ke ADVANCE ($90/bulan, 50 thread) ke atas. Cek halaman pricing CaptchaAI untuk daftar tier terbaru.

Masalah yang muncul saat suite dijalankan

Gejala Penyebab Penanganan
Tantangan muncul di setiap tes login Sesi tidak dipakai ulang Autentikasi sekali, jalankan seluruh tes dalam satu session
Pengambilan KRS gagal padahal token valid Token CSRF sudah kedaluwarsa Ambil token CSRF baru bersamaan dengan solve CAPTCHA
Data uji hilang antar-run Portal membersihkan KRS uji Pakai lingkungan pengujian khusus, bukan produksi
sitekey reCAPTCHA tidak ditemukan Portal SPA merender widget secara dinamis Pakai otomatisasi browser untuk membaca DOM setelah render
Token ditolak portal pageurl tidak sama dengan URL halaman widget Kirim URL halaman yang persis, termasuk path lengkap

Untuk portal yang memakai reCAPTCHA v2 di halaman login dan Turnstile di perencana jadwal, baca penanganan Turnstile dan reCAPTCHA v2 di situs yang sama agar helper Anda memilih method yang tepat per widget.

Pertanyaan umum

Apakah pola ini boleh dipakai pada portal kampus mana pun?

Tidak. Gunakan hanya pada sistem milik institusi Anda atau yang Anda punya izin uji tertulis, dengan akun dan data uji sintetis. Menguji portal pihak ketiga tanpa izin bukan skenario QA.

Berapa lama satu solve reCAPTCHA v2 memperlambat sebuah tes?

Tambahan waktunya adalah durasi penyelesaian ditambah interval polling Anda. Ukur baseline tes tanpa CAPTCHA dulu, lalu tetapkan batas waktu suite dengan menyertakan selisih itu agar tes tidak gagal hanya karena kehabisan waktu.

Bagaimana kalau portal kampus memakai hCaptcha, bukan reCAPTCHA v2?

hCaptcha dan FunCaptcha tidak didukung CaptchaAI saat ini, dan GeeTest v4 berstatus segera hadir. Jadwalkan skenario itu sebagai tes manual, lalu otomatiskan sisa alurnya.

Bagaimana menjalankannya di pipeline CI/CD kampus?

Simpan API key sebagai secret pipeline, bukan di repository, lalu baca lewat environment variable. Tambahkan CaptchaAI sebagai dependensi tahap pengujian, dan batasi job paralel sesuai jumlah thread paket Anda supaya antrean solve tidak menumpuk.

Bisakah runner ini dipakai untuk uji beban masa KRS?

Bisa, dengan catatan throughput dibatasi jumlah thread paket Anda, bukan jumlah worker di skrip. Jalankan job di region terdekat — AWS ap-southeast-3 (Jakarta) atau ap-southeast-1 (Singapura) — supaya latensi jaringan tidak bercampur dengan waktu penyelesaian CAPTCHA.

Artikel Terkait

Langkah Selanjutnya

Petakan titik CAPTCHA di portal Anda, lalu ambil API key CaptchaAI dan jalankan skenario KRS pertama dari awal sampai akhir tanpa satu klik manual.

Komentar dinonaktifkan untuk artikel ini.