Testes de Rate-Limit — Mural Olímpico
Documentação do script
scripts/test-portal-rate-limit.mjse sistema de relatórios.
Visão Geral
O script simula picos de acesso reais (ex: 1.500 alunos em 3-4 minutos) ao Mural Olímpico (Método A: matrícula + data de nascimento) para validar:
- Rate limits (por escola e por IP)
- Lockout progressivo (por identificador/matrícula)
- Estabilidade do banco sob carga massiva
- Anti-timing e mensagens genéricas (sem enumeração)
Pré-requisitos
- Node 18+ (fetch nativo)
- Edge Function
portal-escoladeployada no Supabase - Dados de alunos (opcional):sqlSalvar como JSON:
SELECT matricula, data_nascimento FROM alunos WHERE escola_id = '00000000-0000-0000-0000-000000000003' AND ativo = true;[{"matricula":"001","dataNascimento":"2012-05-15"}, ...] - Limpar tentativas anteriores (para resultados limpos):sql
DELETE FROM portal_login_tentativas WHERE escola_id = '00000000-0000-0000-0000-000000000003';
Uso
# Todos os cenários (com pausas de 5min entre cada)
node scripts/test-portal-rate-limit.mjs
# Cenário específico
node scripts/test-portal-rate-limit.mjs --only=legitimo
node scripts/test-portal-rate-limit.mjs --only=bruteforce
node scripts/test-portal-rate-limit.mjs --only=inexistente
node scripts/test-portal-rate-limit.mjs --only=stress
# Com dados reais de alunos
node scripts/test-portal-rate-limit.mjs --alunos=alunos.json
# Ajustar concorrência
node scripts/test-portal-rate-limit.mjs --concurrency=100
# Pausa personalizada entre cenários (em segundos)
node scripts/test-portal-rate-limit.mjs --pause=120
# Dry run (não envia requests)
node scripts/test-portal-rate-limit.mjs --dry-runCenários
Cenário 1: Tráfego Legítimo (sem retry)
- Envia
TOTAL_STUDENTSrequests simultâneos com dados de alunos - Mede quantos passam (200) vs quantos são bloqueados (429)
- Esperado: Maioria passa, rate limit ativa em picos >600 req/min
Cenário 1B: Legítimo com Retry
- Mesmo que o 1, mas com retry automático ao receber 429
- Simula comportamento real do frontend
- Requer
--alunos=arquivo.jsoncom dados reais - Esperado: 100% dos alunos conseguem logar eventualmente
Cenário 2: Brute Force (lockout progressivo)
Testa o lockout em 3 fases com espera entre cada:
| Fase | Tentativas | Lockout | Espera |
|---|---|---|---|
| 1 | 3 erros | 1 minuto | 70s |
| 2 | +3 erros (total 6) | 5 minutos | 310s |
| 3 | +4 erros (total 10) | 30 minutos | — |
- Após fase 3, testa se login CORRETO também é bloqueado (lockout por identificador, não por dados)
- Duração total: ~7 minutos
- Esperado: 429 detectado em cada transição de tier
Nota sobre acumulação: O lockout é cumulativo em janela de 24h. Se rodar o cenário 2x seguidas sem limpar o banco, o segundo teste já começa com falhas acumuladas e pode pular tiers.
Cenário 3: Matrícula Inexistente
- Envia 20 requests com matrículas fictícias
- Verifica se as respostas são genéricas (sem revelar se matrícula existe)
- Esperado: Todas as respostas HTTP 401 com mensagem idêntica
Cenário 4: Stress (burst máximo)
- Dispara 1.200 requests simultaneamente (sem pool de concorrência)
- Simula o pior caso possível de burst
- Esperado: 0 erros 500, 0 erros de rede, rate limit contém o burst
Sistema de Relatórios
Arquitetura
scripts/
├── test-portal-rate-limit.mjs ← Roda testes, exporta JSON
├── gerar-relatorio.mjs ← Lê JSON, gera .md
└── relatorios/ ← Criada automaticamente
├── resultados_stress_2026-03-08_03h45.json
└── relatorio_stress_2026-03-08_03h45.mdFluxo Automático
- O script de teste executa os cenários
- Ao finalizar, salva resultados em
scripts/relatorios/resultados_[cenário]_[data].json - Chama automaticamente
gerar-relatorio.mjsque gera o.mdna mesma pasta
Nomes dos Arquivos
Formato: relatorio_[cenário]_YYYY-MM-DD_HHhMM.md
Exemplos:
relatorio_bruteforce_2026-03-08_03h45.mdrelatorio_todos_2026-03-08_14h20.mdrelatorio_stress_2026-03-09_09h00.md
Gerar Relatório Manualmente
node scripts/gerar-relatorio.mjs scripts/relatorios/resultados_stress_2026-03-08_03h45.jsonConteúdo do Relatório MD
- Configuração: Gateway, escola, concorrência, período
- Resumo por cenário: Tabela com contagens de status, duração, throughput, veredicto PASS/FAIL
- Latência: p50/p95/p99/max por cenário
- Rate limit breakdown: Contagem por tipo (escola, IP, lockout)
- Lockouts detectados: Tentativa e mensagem (cenário brute force)
- Veredicto final: PASS se 0 erros 500 e 0 erros de rede em todos os cenários
Interpretação de Resultados
Veredicto PASS/FAIL
- PASS: 0 erros HTTP 500, 0 erros de rede. Rate limits podem ter ativado (esperado).
- FAIL: Erros 500 ou falhas de rede indicam instabilidade (pool de conexões, timeout, crash).
Erros Comuns
| Sintoma | Causa Provável | Solução |
|---|---|---|
| Muitos erros de rede (status 0) | Pool de conexões esgotado | Verificar singleton createSupabaseSystem |
| 500s em burst | Edge Function crashou | Verificar logs Supabase |
| Rate limit não ativa | Contadores zerados | Verificar portal_login_tentativas |
| Lockout não detectado | Tentativas anteriores na janela 24h | Limpar tabela antes do teste |
| Todos os cenários 429 | Teste anterior poluiu contadores | DELETE FROM portal_login_tentativas WHERE escola_id = '...' |
Otimizações de Infraestrutura
Singleton createSupabaseSystem()
O cliente System é cacheado em variável de módulo (singleton por Deno isolate). Isso evita a criação de múltiplas conexões por request, reduzindo o consumo de pool de ~6x para ~1x por request.
Fire-and-Forget em Writes de Sucesso
No path de sucesso do login, registrarTentativa e registrarLog executam sem await, retornando a resposta ao aluno imediatamente.
Referências
- Rate-Limits e Proteção Anti-Brute-Force
- Mural Olímpico — Overview
- Edge Function:
supabase/functions/portal-escola/index.ts - Script:
scripts/test-portal-rate-limit.mjs - Gerador:
scripts/gerar-relatorio.mjs