Para testar um portal de matrícula protegido por reCAPTCHA v2 sem clicar em nada à mão, a suíte extrai a sitekey da página, envia o desafio à API da CaptchaAI, recebe o token e o injeta no campo g-recaptcha-response do próprio formulário. É o mesmo ciclo do login, da busca de turmas e da confirmação de vaga — muda só a URL.
Vale resolver isso cedo. Toda janela de matrícula em universidade brasileira tem o mesmo perfil: dezenas de milhares de alunos entram na mesma madrugada, e é na véspera que o QA precisa da suíte rodando sozinha, validando lista de espera e vagas. Um CAPTCHA no caminho vira trabalho manual — ou um teste que alguém marca como "pular" e nunca mais roda.
Onde o reCAPTCHA aparece no portal de matrícula
Antes de escrever qualquer teste, mapeie os pontos de verificação — eles se concentram nas telas que consultam ou alteram dados de vaga:
| Etapa do fluxo | Tipo de CAPTCHA | Por que a instituição protege |
|---|---|---|
| Login do aluno | reCAPTCHA v2 | Conter tentativas em massa de credenciais |
| Busca e consulta de turmas | reCAPTCHA v2 | Limitar consultas automatizadas ao catálogo |
| Inclusão e cancelamento de disciplinas | reCAPTCHA v2 | Evitar matrícula automatizada |
| Entrada na lista de espera | reCAPTCHA v2 | Barrar registro automático em massa |
| Montagem de grade horária | Cloudflare Turnstile | Proteger a ferramenta interativa |
| Pedido de histórico escolar | reCAPTCHA v2 | Confirmar que há uma pessoa na sessão |
Repare que a grade horária costuma usar Cloudflare Turnstile, não reCAPTCHA. Os dois são suportados pela CaptchaAI, mas o campo de retorno muda — cf-turnstile-response no Turnstile, g-recaptcha-response no reCAPTCHA v2 — e trocar um pelo outro é a causa mais comum de teste que resolve o desafio e mesmo assim recebe erro de validação.
Classe de teste em Python para o fluxo de matrícula
A classe abaixo cobre as quatro operações que toda suíte de matrícula exercita: login, busca de turmas, efetivação da matrícula e leitura de vagas. Ela mantém uma requests.Session viva e só resolve o desafio quando ele aparece na resposta.
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
O polling consulta o resultado a cada 3 segundos, até 60 vezes — o teto de SLA do reCAPTCHA v2 é de menos de 60 s, e a margem evita falso negativo no pico. Se _extract_site_key levantar ValueError, o portal monta o widget por JavaScript e será preciso automação de navegador.
Executor de suíte em JavaScript para o pipeline de 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})`);
Quanto CAPTCHA cada cenário realmente gera
Nem todo teste bate no desafio com a mesma frequência. Saber onde está a concentração ajuda a decidir o que roda a cada commit e o que fica para a noite:
| Cenário da suíte | Frequência do desafio | O que isso significa na execução |
|---|---|---|
| Login com vários perfis de aluno de teste | Alta | Cada tentativa passa pelo widget |
| Matrículas concorrentes na mesma turma | Alta | A proteção anti-bot reage ao paralelismo |
| Buscas de turmas em sequência rápida | Moderada | O portal aplica limite de requisições |
| Montagem de grade horária | Baixa | Interação única dentro da sessão |
| Validação de pré-requisitos | Baixa | Vem embutida no fluxo de matrícula |
A cobrança da CaptchaAI é por thread simultânea, com resoluções ilimitadas — o que importa não é quantos CAPTCHAs a suíte gera no mês, e sim quantos ela resolve ao mesmo tempo. Uma regressão com cinco sessões em paralelo cabe no BASIC (US$ 15/mês, 5 threads); um teste de carga com cinquenta alunos simultâneos pede o ADVANCE (US$ 90/mês, 50 threads).
Problemas comuns e como corrigir
| Sintoma | Causa provável | Correção |
|---|---|---|
| O desafio reaparece a cada teste de login | A sessão não está sendo reaproveitada | Autentique uma vez e rode os testes dentro da mesma sessão |
| A matrícula falha logo depois de resolver o CAPTCHA | O token CSRF expirou enquanto o desafio era resolvido | Releia o token CSRF da página ao enviar o token do CAPTCHA |
| Os dados de teste somem entre execuções | O portal libera as matrículas de teste | Use um ambiente de homologação dedicado, com dados fictícios |
ValueError: reCAPTCHA site key not found |
A página é uma SPA e monta o widget por JavaScript | Use automação de navegador e leia o DOM já renderizado |
| O token volta certo, mas o formulário recusa | Campo errado ou pageurl diferente da página real |
Confira o par tipo/campo e use a URL exata da página onde o widget aparece |
Escopo autorizado: teste o que é seu
Este fluxo pressupõe autorização formal da instituição — contrato de manutenção, homologação interna ou janela acordada com a área de TI. Use contas de aluno fictícias, nunca dados reais: no Brasil, matrícula, CPF e histórico escolar são dados pessoais sob a LGPD, e em Portugal valem as obrigações do RGPD. Homologação com massa fictícia resolve os dois lados — não expõe dado pessoal nem polui o sistema real com matrículas de teste.
Perguntas frequentes
Quanto tempo a suíte perde esperando cada resolução?
O teto de SLA é de menos de 60 s no reCAPTCHA v2 e menos de 10 s no Cloudflare Turnstile. Como o custo é por thread simultânea, o caminho para encurtar a execução é aumentar o paralelismo, não acelerar cada chamada isolada.
E se o portal não usar reCAPTCHA, mas hCaptcha?
Aí este fluxo não se aplica: o hCaptcha não é suportado, assim como o FunCaptcha e o GeeTest v4 (previsto, ainda não disponível). Os tipos cobertos são reCAPTCHA v2 e v3, Cloudflare Turnstile e Cloudflare Challenge, GeeTest v3 e CAPTCHAs de imagem/OCR.
Preciso de um navegador ou basta a API?
Depende de como a página monta o widget. Se a sitekey vier no HTML do servidor, requests e a API bastam. Em portais que renderizam por JavaScript, use Selenium ou Playwright só para ler a sitekey e enviar o token.
Como guardar a chave de API dentro do pipeline?
Como qualquer segredo do CI: variável de ambiente vinda do cofre do runner, nunca no arquivo de teste versionado. Nos exemplos o valor aparece como YOUR_API_KEY justamente por isso.
Vale manter esses testes rodando fora da janela de matrícula?
Sim, e é aí que eles pagam a conta. Fora do pico a suíte vira monitoramento: se a instituição trocar o tipo de CAPTCHA de uma tela ou renomear um campo, o teste noturno acusa a mudança semanas antes das inscrições.
Artigos relacionados
- Como resolver o callback do reCAPTCHA v2 pela API
- reCAPTCHA v2 e Turnstile convivendo no mesmo site
- Como funciona o mecanismo de callback do reCAPTCHA v2
Próximos passos
Deixe a suíte de matrícula rodar de ponta a ponta sem parar no desafio: obtenha sua chave de API da CaptchaAI e resolva o primeiro reCAPTCHA v2 nesta execução.