Aqui está um teste de assinatura de webhook que passa, parece responsável e não prova quase nada:
const ts = Math.floor(Date.now() / 1000).toString();
const sig = await hmacHex(`${ts}.${body}`, secret, "SHA-256");
const headers = new Headers({ "stripe-signature": `t=${ts},v1=${sig}` });
expect(await verifySignature("stripe", headers, body, secret)).toBe(true);
A fixture é montada com a mesma fórmula que o verificador confere. Se você leu errado a documentação da Stripe quando escreveu o verificador (assinou a string errada, juntou com o delimitador errado, usou hex onde deveria ser base64), seu teste codifica o mesmo erro de leitura e os dois continuam verdes. Você não testou "verificamos assinaturas da Stripe". Você testou "concordamos com nós mesmos".
Tínhamos 70 testes exatamente assim, cobrindo 20 provedores. Todos passavam. Então fizemos a única pergunta que importa: uma assinatura produzida pelo próprio código do provedor passaria no nosso verificador?
Faça a verificação cruzada com o SDK do provedor
Alguns provedores incluem uma rotina de assinatura no SDK oficial, normalmente exatamente para isso. A da Stripe é a função generateTestHeaderString; a classe Webhook do Svix tem o método .sign(). Se esses SDKs já são dependências suas, a verificação cruzada não custa nada:
import Stripe from "stripe";
import { Webhook } from "svix";
import { verifySignature } from "./index";
it("accepts a header generated by stripe-node", async () => {
const stripe = new Stripe("sk_test_" + "0".repeat(24));
const payload = JSON.stringify({ id: "evt_1", type: "charge.succeeded" });
const secret = "whsec_" + "s".repeat(32);
const header = stripe.webhooks.generateTestHeaderString({ payload, secret });
expect(
await verifySignature("stripe", new Headers({ "stripe-signature": header }), payload, secret)
).toBe(true);
});
it("accepts a signature generated by the svix SDK", async () => {
const secret = "whsec_" + Buffer.from("k".repeat(24)).toString("base64");
const payload = JSON.stringify({ type: "email.received" });
const msgId = "msg_crosscheck";
const ts = new Date();
const signature = new Webhook(secret).sign(msgId, ts, payload);
const headers = new Headers({
"svix-id": msgId,
"svix-timestamp": Math.floor(ts.getTime() / 1000).toString(),
"svix-signature": signature,
});
expect(await verifySignature("svix", headers, payload, secret)).toBe(true);
});
Agora a asserção é que concordamos com a Stripe, não que concordamos com nós mesmos. Adicione o caso negativo (assine um payload e depois verifique um corpo diferente) para saber que o teste consegue de fato falhar:
const header = stripe.webhooks.generateTestHeaderString({ payload, secret });
const tampered = JSON.stringify({ id: "evt_1", amount: 999999 });
expect(
await verifySignature("stripe", new Headers({ "stripe-signature": header }), tampered, secret)
).toBe(false);
Como o Clerk e o Resend entregam pela infraestrutura do Svix, uma verificação cruzada com o Svix cobre três provedores. Quatro dos nossos vinte verificadores agora têm prova baseada em SDK, e o mesmo trabalho de uma tarde se aplica a qualquer stack.
E os provedores sem rotina de assinatura pública?
Shopify, Discord, PayPal: a maioria dos provedores não oferece uma rotina de assinatura. Para eles, a única evidência honesta é uma entrega real capturada. Aponte um webhook para um endpoint que você controla, grave uma requisição assinada e congele a tripla (headers, body, secret) como fixture. Uma assinatura real reaproveitada no teste é a verdade de referência que nenhuma fixture montada por você consegue ser.
Até fazer isso, seja preciso consigo mesmo sobre o que seus testes mostram. Mantemos esse controle num comentário no fim do arquivo de testes: quais provedores são verificados por SDK, quais são só autoconsistentes e quais não têm nenhum teste positivo. Uma lista honesta com a qual você pode agir vale mais que uma suíte verde em que você não pode confiar.
O bug que essa mentalidade encontrou no mesmo dia
A mesma desconfiança vale para além dos testes unitários. Enquanto verificávamos de ponta a ponta nosso caminho do Resend contra a produção (não a suíte unitária, a coisa real em produção), enviamos três requisições para um endpoint com um segredo de assinatura configurado:
| Requisição | Esperado | Recebido |
|---|---|---|
| Assinatura correta | 200 | 200 |
| Assinatura errada | 401 | 401 |
| Sem assinatura nenhuma | 401 | 200 |
Uma requisição sem assinatura passou direto por um endpoint que parecia protegido. A causa era uma brecha de política, não um bug de criptografia. Os apps do AnyHook (uma URL de entrada para uma origem) com origem generic pulam a verificação de propósito ("só quero uma URL"), e configurar um segredo não tirava o app de generic. O segredo ficava lá sem verificar nada, enquanto a API respondia sucesso por tê-lo armazenado.
Nenhuma quantidade de testes unitários autoconsistentes encontra isso, porque cada unidade estava correta. A correção foi recusar de vez a configuração enganosa: definir um segredo de assinatura num app generic agora dá um 400 que diz qual campo definir no lugar.
O checklist
- Para cada provedor cujo SDK tem uma rotina de assinatura: verificação cruzada nos dois sentidos (a válida passa, a adulterada falha)
- Para o resto: capture uma entrega real assinada e congele-a como fixture
- Mantenha um registro visível de quais provedores têm qual nível de prova
- Teste o sistema em produção com a ausência de assinatura, não só com assinaturas erradas. As brechas moram na política, não na matemática