Webhook Testing Standard
Padrão SSOT para testes de webhooks de provedores externos (Resend, Wasender, MercadoPago, Twilio, etc.). Toda função
*-webhookemsupabase/functions/DEVE seguir este padrão.
Princípios
- Black-box contra staging — testes batem na Edge Function real deployada (não import direto). Garante que CORS, headers, body parsing e env vars estão corretos no runtime real.
- HMAC obrigatório — toda request sem assinatura ou com assinatura inválida deve retornar 401.
- Idempotência — mesmo evento processado 2× não duplica side-effect (linha em log/outbox/etc.).
- Tolerance window — timestamp fora de janela (default 5min) → 401 (anti-replay).
- PII masking — todo log gerado pelo webhook usa helpers de
_shared/pii-helpers.ts. - Erro de provider externo nunca vira 500 — webhook responde 200 + log
incidente. Provedores (MP, Resend) retentam agressivamente em 5xx, gerando duplicação.
Estrutura padrão
supabase/functions/<provider>-webhook/
├── index.ts
└── __tests__/
├── index.test.ts # smoke + CORS + método
├── hmac.test.ts # assinatura inválida/ausente/expirada
└── idempotency.test.ts # mesmo evento 2× = mesmo estado finalCobertura mínima por suite
index.test.ts — Smoke
OPTIONSretorna CORS válido.- Método não permitido → 405 ou 200 ignored conforme contrato do provider.
- Body inválido (JSON malformado) → 400.
- Resposta sempre tem
{ success: boolean, ... }.
hmac.test.ts — Assinatura
- Sem header de assinatura → 401.
- Assinatura malformada → 401.
- Assinatura válida mas timestamp > 5min → 401.
- Assinatura válida + timestamp em janela → 200 (mesmo que evento seja ignorado por outros motivos).
- Compara em timing-safe (proteção contra timing attack).
idempotency.test.ts — Side-effects
- Enviar mesmo evento 2× com 1s de intervalo.
- Verificar via SQL (read-only) que tabela alvo (
email_eventos,mensagens_log, etc.) tem exatamente 1 linha — ou que upsert não criou duplicata. - Status final do registro é o esperado (não regrediu).
Template de teste (resend-webhook)
typescript
import "https://deno.land/std@0.224.0/dotenv/load.ts";
import { assertEquals } from "https://deno.land/std@0.168.0/testing/asserts.ts";
const SUPABASE_URL = Deno.env.get("VITE_SUPABASE_URL")!;
const ANON = Deno.env.get("VITE_SUPABASE_PUBLISHABLE_KEY")!;
const URL = `${SUPABASE_URL}/functions/v1/resend-webhook`;
Deno.test("resend-webhook: sem svix-* headers → 401", async () => {
const res = await fetch(URL, {
method: "POST",
headers: { "Content-Type": "application/json", "apikey": ANON },
body: JSON.stringify({ type: "email.delivered", data: { email_id: "x" } }),
});
assertEquals(res.status, 401);
await res.text();
});
Deno.test("resend-webhook: timestamp expirado → 401", async () => {
const oldTs = String(Math.floor(Date.now() / 1000) - 600); // 10min atrás
const res = await fetch(URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": ANON,
"svix-id": "msg_test",
"svix-timestamp": oldTs,
"svix-signature": "v1,xxxx",
},
body: "{}",
});
assertEquals(res.status, 401);
await res.text();
});Helpers compartilhados
Crie helpers em supabase/functions/_shared/__tests__/webhook-helpers.ts para:
- Gerar assinatura Svix válida com secret de teste.
- Gerar
x-signatureMercadoPago (ts=...,v1=...). - Aguardar processamento background (poll em tabela alvo com timeout).
Anti-patterns
❌ Importar index.ts direto e chamar Deno.serve — perde validação de CORS e env do runtime. ❌ Mockar crypto.subtle — preferir gerar assinatura real com secret de teste. ❌ Esperar 5xx de webhook em qualquer cenário — retorna 200/4xx sempre, NUNCA 500. ❌ Asserções dependentes de tempo real sem clock.tick — usar timestamps explícitos.
Checklist pré-PR
- [ ] 3 suites criadas (
index,hmac,idempotency). - [ ] Todas rodam em CI (
deno-testsjob em.github/workflows/ci.yml). - [ ] Secret de teste documentado em
docs/staging/STAGING_REFERENCE.md. - [ ] PII masking validado em logs gerados (
registrarLogusa helpers). - [ ] Atualizado
TESTING_BACKLOG.mdmovendo item para Histórico.
Referências
- Padrão Deno contract:
mem://testing/edge-function-deno-contract-testing - Edge functions test location:
mem://infrastructure/supabase/edge-function-test-location-standard - Sanitização de erros:
mem://architecture/backend-error-sanitization-contextual-standard - LGPD PII helpers:
mem://security/pii-log-masking-standard