عندما يصطدم اختبار E2E بصفحة محمية بـ CAPTCHA داخل خط CI/CD، يتوقف المسار في انتظار تدخل بشري لن يأتي أبداً، فيسقط الاختبار في كل تشغيل. الحل المباشر: خزّن مفتاح CaptchaAI API كسرّ داخل بيئة CI، ودَع مجموعة الاختبار تطلب رمز الحل برمجياً وتحقنه في النموذج ثم تتابع تنفيذها دون توقف. النتيجة اختبارات انحدار كاملة تمرّ على مسارات تسجيل الدخول والنماذج المحمية تلقائياً، بالجودة نفسها التي يتوقعها مهندس ضمان الجودة من بقية المجموعة.
لماذا تسقط اختبارات CI عند أول صفحة CAPTCHA
خطوط CI/CD مصمَّمة لتعمل من دون يد بشرية على الإطلاق، بينما وُضِع اختبار CAPTCHA أصلاً ليطلب تفاعلاً بشرياً. هذا التعارض هو جوهر المشكلة: أي سيناريو E2E يمرّ بنموذج تسجيل دخول أو نموذج تواصل محمي سيتجمّد عند الويدجت، وينتهي بمهلة انتهاء أو فشل تأكيد.
الحلول الالتفافية الشائعة كلها هشّة. تعطيل CAPTCHA في بيئة staging يعني أنك تختبر مساراً مختلفاً عمّا يراه المستخدم في الإنتاج. وإدراج مفاتيح اختبار مكشوفة يترك ثغرة قد تتسرب. الأسلوب الأنظف أن تُبقي الصفحة على حالتها الحقيقية، وتضيف إلى المجموعة خدمة حل تُرجِع رمزاً صالحاً أثناء التشغيل: يُخزَّن مفتاح CaptchaAI API كسرّ ضمن CI، فتحصل الاختبارات على الرمز وتُكمل السيناريو كما لو أن مستخدماً حقيقياً اجتاز التحقق.
هذا الدليل موجَّه لمثالين شائعين في السوق: reCAPTCHA v2 على شاشة تسجيل الدخول، وCloudflare Turnstile على نموذج التواصل — وكلاهما مدعوم بشكل كامل في CaptchaAI.
كيف يتدفق الحل داخل المسار
قبل كتابة أي سطر، تخيّل مسار البيانات: يبدأ التشغيل من دفعة Git، يلتقطه مشغّل CI الذي يفتح Chrome في وضع headless، تصل الاختبارات إلى الصفحة المحمية فتستدعي CaptchaAI للحصول على الرمز، ثم يُحقن الرمز ويكتمل السيناريو ويُرفع تقرير الاختبار.
┌──────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Git Push │────▶│ CI Runner │────▶│ E2E Tests │────▶│ Test Report │
│ │ │ (headless │ │ + CAPTCHA │ │ │
│ │ │ Chrome) │ │ solving │ │ │
└──────────────┘ └──────────────┘ └────────────┘ └──────────────┘
│
▼
┌────────────┐
│ CaptchaAI │
│ API │
└────────────┘
نقطة الاتصال الوحيدة بين اختباراتك والخدمة هي استدعاء واحد يُرسِل بيانات الويدجت ويستطلع النتيجة. نُغلِّف هذا الاستدعاء في مساعد صغير حتى تبقى ملفات الاختبار نظيفة.
بناء مساعد حل CAPTCHA للـ CI
المساعد التالي يقرأ المفتاح من متغيّر البيئة CAPTCHAAI_API_KEY، يُرسِل المهمة إلى نقطة النهاية in.php، ثم يستطلع res.php حتى يجهز الرمز أو تنتهي المهلة. صُمِّم خصيصاً لبيئة CI: يفشل بوضوح إذا كان المفتاح غير مضبوط، ويرفع خطأ مهلة صريحاً بدل التعليق إلى ما لا نهاية.
import os
import time
import requests
class CICaptchaSolver:
"""CAPTCHA solver designed for CI environments."""
BASE = "https://ocr.captchaai.com"
def __init__(self):
self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not self.api_key:
raise EnvironmentError("CAPTCHAAI_API_KEY not set")
def solve(self, params, initial_wait=10, timeout=120):
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(f"CAPTCHA submit failed: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
deadline = time.time() + timeout
while time.time() < deadline:
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(f"CAPTCHA solve failed: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out in CI")
def solve_recaptcha(self, sitekey, pageurl):
return self.solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
})
def solve_turnstile(self, sitekey, pageurl):
return self.solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
لاحظ أن الرمز الناتج عن reCAPTCHA يُحقن لاحقاً في الحقل g-recaptcha-response، بينما يذهب رمز Turnstile إلى الحقل cf-turnstile-response — لكل نوع حقله الخاص، ولا يجوز الخلط بينهما.
الدمج مع pytest
نربط المساعد بمجموعة الاختبار عبر تجهيزتين (fixtures): واحدة تُنشئ الحلّال مرة واحدة لكل جلسة، وأخرى تفتح متصفح Chrome بلا واجهة لكل اختبار على حدة. أعلام --no-sandbox و--disable-dev-shm-usage ضرورية لتشغيل Chrome بثبات داخل حاويات CI.
conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="session")
def captcha_solver():
return CICaptchaSolver()
@pytest.fixture(scope="function")
def browser():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=options)
driver.set_window_size(1920, 1080)
yield driver
driver.quit()
ملف الاختبار
يغطّي هذا الملف سيناريوهين واقعيين: تسجيل دخول محمي بـ reCAPTCHA v2 (بحالتَي نجاح وكلمة مرور خاطئة)، ونموذج تواصل محمي بـ Cloudflare Turnstile. في كل حالة يُطلب الرمز من CaptchaAI، ثم يُحقن في الحقل المناسب عبر execute_script، ثم يُرسَل النموذج ويُتحقق من النتيجة.
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestLoginFlow:
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
LOGIN_URL = "https://staging.example.com/login"
def test_login_with_captcha(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
# Fill credentials
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("testpass123")
# Solve CAPTCHA
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
# Submit
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
# Verify login success
assert "dashboard" in browser.current_url.lower()
def test_login_wrong_password(self, browser, captcha_solver):
browser.get(self.LOGIN_URL)
browser.find_element(By.ID, "username").send_keys("testuser")
browser.find_element(By.ID, "password").send_keys("wrongpass")
token = captcha_solver.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
browser.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
browser.find_element(By.ID, "login-btn").click()
time.sleep(3)
error = browser.find_element(By.CSS_SELECTOR, ".error-message")
assert error.is_displayed()
class TestContactForm:
SITEKEY = "0x4AAAA..."
FORM_URL = "https://staging.example.com/contact"
def test_contact_form_submission(self, browser, captcha_solver):
browser.get(self.FORM_URL)
browser.find_element(By.ID, "name").send_keys("CI Test")
browser.find_element(By.ID, "email").send_keys("ci@test.com")
browser.find_element(By.ID, "message").send_keys("Automated CI test")
token = captcha_solver.solve_turnstile(self.SITEKEY, self.FORM_URL)
browser.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
browser.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
)
تشغيل الاختبارات في GitHub Actions
يمرّر سير العمل التالي مفتاح API إلى الاختبارات عبر secrets.CAPTCHAAI_API_KEY، فلا يظهر المفتاح في الكود ولا في السجلّات. يُثبِّت Chrome وChromeDriver، يشغّل مجموعة E2E، ثم يرفع تقرير HTML كأثر (artifact) حتى في حال الفشل بفضل if: always().
name: E2E Tests with CAPTCHA
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: Install ChromeDriver
uses: nanasess/setup-chromedriver@v2
- name: Install dependencies
run: |
pip install selenium requests pytest pytest-html
- name: Run E2E tests
env:
CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
run: |
pytest tests/e2e/ -v --html=report.html --self-contained-html
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-report
path: report.html
الإعداد في GitLab CI
على GitLab يُشغَّل Chrome كخدمة منفصلة، ويُقرأ المفتاح من متغيّرات المشروع المحمية. لا تُدرِج CAPTCHAAI_API_KEY في الملف مطلقاً؛ عرّفه من إعدادات المشروع كمتغيّر مقنَّع (masked).
e2e_tests:
stage: test
image: python:3.11
services:
- selenium/standalone-chrome:latest
variables:
SELENIUM_REMOTE_URL: "http://selenium__standalone-chrome:4444/wd/hub"
script:
- pip install selenium requests pytest
- pytest tests/e2e/ -v
artifacts:
when: always
reports:
junit: report.xml
التكامل مع Jenkins
يستخدم المسار التالي مخزن بيانات الاعتماد في Jenkins عبر credentials('captchaai-api-key')، فيُحقن المفتاح في البيئة دون كتابته صراحةً. تُنشر نتائج JUnit في مرحلة post لتظهر في لوحة النتائج حتى عند فشل الاختبارات.
pipeline {
agent any
environment {
CAPTCHAAI_API_KEY = credentials('captchaai-api-key')
}
stages {
stage('Setup') {
steps {
sh 'pip install selenium requests pytest'
}
}
stage('E2E Tests') {
steps {
sh 'pytest tests/e2e/ -v --junitxml=results.xml'
}
}
}
post {
always {
junit 'results.xml'
}
}
}
التحكم في تكلفة الحل داخل CI
تعتمد خطط CaptchaAI على عدد الـ threads المتزامنة لا على عدد عمليات الحل؛ فباقة BASIC تبدأ من 15 دولاراً شهرياً بخمسة threads مع عدد غير محدود من عمليات الحل خلال الشهر، وترتقي الباقات حتى VIP-3 بسعر 7,500 دولار و5,000 thread. هذا النموذج مريح لفرق CI: طالما بقيت عمليات الحل المتزامنة ضمن حصة الـ threads، فلن تدفع مقابل كل عملية حل على حدة.
تخيّل فريق ضمان جودة في شركة تجارة إلكترونية بالرياض يشغّل انحداراً ليلياً على مسارَي تسجيل الدخول وإتمام الشراء. لو حاول تشغيل اختبارات CAPTCHA في كل طلب سحب لعشرات المطورين، لتضخّم عدد عمليات الحل بلا فائدة حقيقية. الأفضل حصر هذه الاختبارات في الدمج إلى الفرع الرئيسي أو في تشغيل ليلي مجدول، وتخطّيها في بناء طلبات السحب اليومية.
حل فقط عند الحاجة
تتيح الدالة التالية إيقاف اختبارات CAPTCHA عبر متغيّر بيئة واحد، أو تخطّيها تلقائياً حين لا يكون المفتاح مضبوطاً — وهو ما يفيد في بناء طلبات السحب (PR builds) حيث لا تريد استهلاك حصة الحل.
import os
def should_run_captcha_tests():
"""Skip CAPTCHA tests in certain environments."""
if os.environ.get("SKIP_CAPTCHA_TESTS"):
return False
if not os.environ.get("CAPTCHAAI_API_KEY"):
return False
return True
# In test
import pytest
@pytest.mark.skipif(
not should_run_captcha_tests(),
reason="CAPTCHA tests disabled or API key not set"
)
class TestWithCaptcha:
def test_login(self, browser, captcha_solver):
pass
التحقق من الرصيد قبل تشغيل المجموعة
تجهيزة autouse التالية تفحص الرصيد مرة واحدة قبل بدء الجلسة، وتتخطّى المجموعة كلها إذا هبط الرصيد عن حدّ آمن — فتتجنّب تشغيلاً كاملاً يفشل في منتصفه بسبب نفاد الرصيد.
@pytest.fixture(scope="session", autouse=True)
def check_captcha_balance(captcha_solver):
import requests
resp = requests.get(
f"{captcha_solver.BASE}/res.php",
params={"key": captcha_solver.api_key, "action": "getbalance"},
)
balance = float(resp.text)
if balance < 0.50:
pytest.skip(f"CaptchaAI balance too low: ${balance:.2f}")
حل مشكلات التشغيل الشائعة
| المشكلة | السبب | الإجراء |
|---|---|---|
CAPTCHAAI_API_KEY not set |
لم يُضبط السر في بيئة CI | أضِف المفتاح إلى أسرار CI |
| يتعطّل Chrome داخل CI | غياب العلامة --no-sandbox |
أضِف أعلام Chrome الخاصة بوضع headless |
| الاختبارات تنجح محلياً وتفشل في CI | اختلاف إصدار المتصفح | ثبّت إصدار Chrome داخل CI |
| انتهاء مهلة حل CAPTCHA | بطء شبكة CI | ارفع قيمة المعامل timeout |
| ارتفاع تكلفة التشغيل | عمليات حل كثيرة في كل تشغيل | استخدم SKIP_CAPTCHA_TESTS لبناء طلبات السحب (PR) |
الأسئلة الشائعة
هل يبطئ حل CAPTCHA زمن تشغيل خط CI بشكل ملحوظ؟
يضيف كل حل بضع ثوانٍ لأن الخدمة تستطلع النتيجة حتى تجهز. لتقليل الأثر، شغّل اختبارات CAPTCHA على التوازي عبر pytest-xdist بحيث تنتظر عدة اختبارات في الوقت نفسه بدل التسلسل، وتعامل CaptchaAI مع الطلبات المتزامنة ضمن حصة الـ threads.
ماذا أفعل إذا تغيّر sitekey في بيئة staging؟
اقرأ مفتاح الموقع من متغيّر بيئة بدل تثبيته في الاختبار، وحدّثه من إعدادات CI عند تغيّر البيئة. هكذا لا تحتاج إلى تعديل الكود، وتبقى الاختبارات صالحة عبر staging والإنتاج.
كيف أمنع تسريب مفتاح API في سجلّات CI؟
استعمل إدارة الأسرار في منصّتك: GitHub Secrets أو GitLab CI Variables (مقنّعة) أو Jenkins Credentials، ولا تضع المفتاح داخل الكود مطلقاً. تُخفي هذه الأدوات القيمة تلقائياً من سجلّات التشغيل.
هل يتعامل CaptchaAI مع reCAPTCHA v2 وTurnstile في المجموعة نفسها؟
نعم. كلا النوعين مدعوم بشكل كامل، وتستدعي كلاً منهما عبر الدالة المناسبة (solve_recaptcha أو solve_turnstile) مع حقن الرمز في حقله الصحيح — g-recaptcha-response أو cf-turnstile-response.
كيف أضبط تكلفة الحل عبر عدد كبير من عمليات التشغيل؟
احصر اختبارات CAPTCHA في الدمج إلى الفرع الرئيسي أو في تشغيل ليلي مجدول، وتخطّها في بناء طلبات السحب عبر SKIP_CAPTCHA_TESTS. وبما أن الفوترة تعتمد على الـ threads لا على عدد عمليات الحل، فإن ضبط التزامن مع حصة باقتك يبقي التكلفة ثابتة ومتوقّعة.
أدلة ذات صلة
أضِف حل CAPTCHA إلى خط CI الخاص بك — ابدأ مع CaptchaAI.