Ders kayıt portalının regresyon testi üçüncü ekranda bir reCAPTCHA v2 onay kutusuna takıldığında iki seçeneğiniz kalır: test paketini durdurup elle tıklamak ya da doğrulamayı programatik olarak çözmek. İkincisi, CaptchaAI API'ye sitekey ve pageurl göndermek ve dönen token'ı formun g-recaptcha-response alanına koymaktan ibarettir — bu sayfadaki Python sınıfı tam olarak bunu yapar. Aşağıda kayıt akışının hangi adımlarında doğrulama çıktığını, test paketini nasıl kurgulayacağınızı ve pratikte hangi hatalara takılacağınızı bulacaksınız.
Senaryo tanıdık: ders kayıt haftası açılır, öğrenci bilgi sistemi birkaç saat boyunca yılın en yüksek eşzamanlı yükünü görür ve yazılım ekibinin bu pencereden önce kayıt, bırakma ve bekleme listesi akışlarını uçtan uca doğrulamış olması gerekir. Manuel CAPTCHA tıklamasına bağlı bir test paketi bu takvime yetişmez.
Kayıt akışında doğrulama hangi adımlarda çıkar
Test kapsamınızı planlarken önce haritayı çıkarın. Tipik bir kayıt portalında doğrulama noktaları şöyle dağılır:
| Kayıt adımı | CAPTCHA türü | Amacı |
|---|---|---|
| Öğrenci giriş sayfası | reCAPTCHA v2 | Kimlik bilgisi denemelerini sınırlamak |
| Ders arama / listeleme | reCAPTCHA v2 | Otomatik sorguları sınırlamak |
| Ders ekleme ve bırakma | reCAPTCHA v2 | Otomatik kaydı engellemek |
| Bekleme listesine yazılma | reCAPTCHA v2 | Otomatik kayıt botlarını engellemek |
| Program planlayıcı | Cloudflare Turnstile | Etkileşimli araçları korumak |
| Transkript talebi | reCAPTCHA v2 | Kullanıcının insan olduğunu doğrulamak |
Çoğu portalda tek tür yoktur: kayıt formları reCAPTCHA v2 ile, planlayıcı gibi etkileşimli araçlar Cloudflare Turnstile ile korunur. Her ikisi de aynı API üzerinden çözülür; değişen tek şey method parametresi ile token'ın yerleştirileceği form alanının adıdır. Test kapsamınızı yazarken bu iki türü ayrı senaryolar olarak işaretleyin, çünkü aynı sayfada birlikte de görünebilirler.
Test paketini kurgularken üç karar
Ortam kararı. Kurum içi bir hazırlık ortamınız varsa doğrulamayı orada kapatmak en az iş çıkaran yoldur. Ancak birçok üniversite portalı dış tedarikçi tarafından işletilir ve doğrulama yapılandırması ekibinizin elinde değildir; bu durumda üretim davranışını olduğu gibi test etmekten başka seçenek kalmaz.
Oturum kararı. Her test için yeniden giriş yapmayın. Bir kez kimlik doğrulayıp aynı requests.Session üzerinden ilerlemek, çözülmesi gereken doğrulama sayısını ve dolayısıyla toplam test süresini belirgin biçimde düşürür.
Kapsam kararı. Kayıt haftası testlerinde en kritik yollar sırayla şunlardır: giriş, ders arama sonuçlarının doğruluğu, kontenjan gösterimi, kayıt ve bekleme listesine geçiş. Paketi bu sırayla kurun; bırakma ve önkoşul kontrolü bir sonraki halkadır.
KVKK notu: test verisi olarak gerçek öğrenci kayıtlarını kullanmayın. Öğrenci numarası, ad ve iletişim bilgisi KVKK kapsamında kişisel veridir; sentetik test hesapları hem mevzuat hem de tekrarlanabilirlik açısından doğru tercihtir.
Kayıt akışını test eden Python sınıfı
Aşağıdaki RegistrationTester oturumu tek noktada tutar, sayfada doğrulama olup olmadığını kontrol eder, gerekirse sitekey'i çıkarıp CaptchaAI'ye gönderir ve dönen token'ı form verisine ekler. Periyodik sorgulama döngüsü üç saniyede bir res.php uç noktasını sorgular ve makul bir zaman aşımıyla biter.
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
Dikkat edilecek üç nokta var. Birincisi, _extract_site_key düz HTML üzerinde çalışır; portal tek sayfa uygulamasıysa sitekey işlenmiş DOM'da oluşur ve bu regex boş döner. İkincisi, token'ın ömrü sınırlıdır — çözümden sonra formu hemen gönderin. Üçüncüsü, _solve_recaptcha her çağrıldığında bir thread meşgul eder; paralel test sayınızı planınızın thread sayısına göre belirleyin.
Paketi çalıştıran JavaScript koşucusu
Node.js tarafında aynı mantığı bir test koşucusu içinde toplayabilirsiniz. Aşağıdaki sınıf testleri sırayla çalıştırır, hataları yakalar ve sonunda geçen/kalan sayısını içeren bir rapor üretir:
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})`);
Bu raporu CI çıktısına yazdırmak, kayıt haftası öncesi her gece çalışan bir işte durumu tek satırda görmenizi sağlar.
Hangi test senaryoları doğrulamayı tetikler
Her test aynı sıklıkta doğrulama üretmez. Aşağıdaki tablo, test paketinin çözüm bütçesini nerede harcadığını gösterir:
| Test senaryosu | CAPTCHA sıklığı | Etkisi |
|---|---|---|
| Birden fazla hesapla giriş | Yüksek | Her giriş denemesi doğrulama üretir |
| Eşzamanlı kayıt testleri | Yüksek | Bot korumasını devreye sokar |
| Hızlı ardışık ders aramaları | Orta | İstek sınırlama devreye girer |
| Program oluşturma | Düşük | Tek oturum içinde tek etkileşim |
| Önkoşul doğrulama | Düşük | Kayıt akışının içinde çözülür |
Buradan çıkan pratik sonuç: giriş testlerini tek bir kimlik doğrulamayla toplulaştırın, arama testlerini ise istekler arasına gecikme koyarak çalıştırın.
Yük testi ve thread planlaması
Kayıt haftası simülasyonu, eşzamanlı öğrenci oturumlarını taklit etmek demektir — ve her oturum kendi doğrulamasını üretir. CaptchaAI thread tabanlı faturalandırır: her plan, thread başına aylık sınırsız çözüm içerir, dolayısıyla maliyetiniz toplam test sayısıyla değil eşzamanlılıkla belirlenir.
Küçük bir QA ekibi için BASIC ($15/ay, 5 thread) seri koşan bir gece paketine yeter. 50 sanal öğrenci oturumunu aynı anda çalıştıran bir yük senaryosunda ADVANCE ($90/ay, 50 thread) doğru ölçektir. Fiyatlar USD üzerindendir ve TL'ye çevrilmez; kur dalgalanmasının olduğu bir pazarda öngörülebilir aylık USD maliyeti, çözüm başına ücretlendirmeye göre bütçelemesi kolay bir modeldir.
Sorun giderme
| Sorun | Sebep | Çözüm |
|---|---|---|
| Her testte yeniden doğrulama çıkıyor | Oturum yeniden kullanılmıyor | Bir kez giriş yapın, testleri aynı oturumda çalıştırın |
| Token geçerli ama kayıt başarısız | CSRF token'ının süresi dolmuş | CSRF token'ını doğrulamayla aynı anda yeniden okuyun |
| Test verileri koşular arasında sıfırlanıyor | Portal test kayıtlarını temizliyor | Ayrılmış bir test ortamı kullanın |
sitekey bulunamadı hatası |
SPA doğrulamayı dinamik oluşturuyor | İşlenmiş DOM'a erişmek için tarayıcı otomasyonu kullanın |
| Çözüm süresi uzuyor | Thread'lerin tamamı meşgul | Eşzamanlı test sayısını plan thread sayısına indirin |
SSS
Ders kayıt haftası yükünü test ederken kaç thread gerekir?
Eşzamanlı sanal öğrenci sayınız kadar. 20 paralel oturum çalıştıran bir senaryoda 20 thread'lik kapasite gerekir; thread'ler dolduğunda istekler kuyruğa girer ve testin toplam süresi uzar.
CaptchaAI kayıt portallarında karşılaştığım tüm doğrulama türlerini çözüyor mu?
Hayır. reCAPTCHA v2 ve v3 (Enterprise varyantları dahil), Cloudflare Turnstile ve Cloudflare doğrulama akışı, GeeTest v3, görüntü/OCR ve grid türleri destekleniyor; CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) beta aşamasında. hCaptcha ve FunCaptcha desteklenmiyor; GeeTest v4 için ise yalnızca "çok yakında" ifadesi geçerli.
Testleri Jenkins veya GitHub Actions içinde çalıştırabilir miyim?
Evet. API anahtarını ortam değişkeni olarak verin ve çözüm çağrısını normal bir test bağımlılığı gibi ele alın. Gece çalışan bir işte kayıt akışının uçtan uca doğrulanması, portalın güvenlik ayarlarına dokunmadan mümkün olur.
Portalın hazırlık ortamında doğrulama kapalıysa yine de bu kuruluma ihtiyacım var mı?
Üretim davranışını doğrulayacaksanız evet. Hazırlık ortamında doğrulama kapalıyken geçen bir akış, üretimde token alanı boş gittiği için başarısız olabilir; en az bir duman testini üretim yapılandırmasıyla çalıştırmak bu farkı yakalar.
İlk entegrasyonu ne kadar sürede kurabilirim?
Anahtarı alıp ilk token'ı görmek birkaç dakika sürer: hesabı açın, panelden anahtarı kopyalayın ve yukarıdaki Python sınıfında api_key değerini değiştirin. İlk çalıştırmayı tek bir giriş testiyle yapın; akış doğrulandıktan sonra paketin geri kalanını ekleyin.
İlgili Makaleler
- reCAPTCHA v2 callback doğrulamasını API ile çözme
- Aynı sayfada reCAPTCHA v2 ve Turnstile'ı birlikte yönetme
- reCAPTCHA v2 callback mekanizması nasıl çalışır
Sonraki Adımlar
Kayıt portalı testlerinizi manuel tıklamaya bağlı kalmadan çalıştırın: CaptchaAI API anahtarınızı alın ve doğrulama adımını test paketinizin içine taşıyın.