|
| 1 | +/** |
| 2 | + * O LINK DO E-MAIL DE AUTH É MONTADO POR DOIS ARQUIVOS QUE NÃO SE CONHECEM. |
| 3 | + * |
| 4 | + * Quem inicia a query é o Server Action (`?type=recovery` / `?type=signup` em |
| 5 | + * `redirectTo`/`emailRedirectTo`); quem a continua é o template HTML que o |
| 6 | + * GoTrue renderiza (`&token_hash={{ .TokenHash }}`). Um é TypeScript, o outro |
| 7 | + * é HTML lido por um serviço em Go — nenhum compilador, tipo ou teste de |
| 8 | + * unidade existente liga os dois. |
| 9 | + * |
| 10 | + * ## O defeito que fez este arquivo existir |
| 11 | + * |
| 12 | + * O template usava `?token_hash=...`. Quando o Action passou a anexar |
| 13 | + * `?type=...` ao redirect (necessário porque, com o template PADRÃO do |
| 14 | + * Supabase, o link chega via PKCE `code` e o `type` não sobrevive ao hop pelo |
| 15 | + * GoTrue), o link virou: |
| 16 | + * |
| 17 | + * /auth/confirm?type=recovery?token_hash=pkce_abc&type=recovery |
| 18 | + * ↑ segundo `?` |
| 19 | + * |
| 20 | + * O parser de URL do browser trata tudo depois do PRIMEIRO `?` como query, e o |
| 21 | + * segundo `?` vira parte do VALOR de `type`. `token_hash` deixa de ser um |
| 22 | + * parâmetro, `/auth/confirm` recebe `null` e manda o usuário para |
| 23 | + * `/login?error=link_invalido` — com um token perfeitamente válido na mão. |
| 24 | + * |
| 25 | + * Nada acusava: `typecheck`, `lint` e `test:unit` passam, porque HTML errado |
| 26 | + * não compila nem é importado. O sintoma que o usuário vê ("link expirado") não |
| 27 | + * se parece com a causa. |
| 28 | + * |
| 29 | + * ## O que se guarda |
| 30 | + * |
| 31 | + * Os DOIS sentidos, porque consertar um lado sozinho reintroduz o bug pelo |
| 32 | + * outro: se o Action parar de abrir a query, o `&` do template passa a ser o |
| 33 | + * separador errado e o link quebra igual. |
| 34 | + */ |
| 35 | +import { readFileSync } from "node:fs"; |
| 36 | +import { join } from "node:path"; |
| 37 | + |
| 38 | +import { describe, expect, it } from "vitest"; |
| 39 | + |
| 40 | +const raiz = process.cwd(); |
| 41 | +const ler = (p: string) => readFileSync(join(raiz, p), "utf8"); |
| 42 | + |
| 43 | +const PARES = [ |
| 44 | + { |
| 45 | + fluxo: "recovery", |
| 46 | + action: "app/actions/auth/requestPasswordReset.ts", |
| 47 | + template: "supabase/templates/recovery.html", |
| 48 | + }, |
| 49 | + { |
| 50 | + fluxo: "signup", |
| 51 | + action: "app/actions/auth/signUp.ts", |
| 52 | + template: "supabase/templates/confirmation.html", |
| 53 | + }, |
| 54 | +] as const; |
| 55 | + |
| 56 | +describe("o link do e-mail de auth tem UMA query só", () => { |
| 57 | + for (const { fluxo, action, template } of PARES) { |
| 58 | + it(`${fluxo}: o Action ABRE a query com ?type=`, () => { |
| 59 | + const fonte = ler(action); |
| 60 | + // Guarda de vacuidade: arquivo movido ou vazio faria o `toMatch` abaixo |
| 61 | + // falhar por ausência de dado, e a mensagem não diria isso. |
| 62 | + expect(fonte.length, `${action} veio vazio — o teste ficou cego`).toBeGreaterThan(200); |
| 63 | + expect( |
| 64 | + fonte, |
| 65 | + `${action} precisa anexar ?type=${fluxo} ao redirect — é o que sobrevive ao hop do GoTrue no formato PKCE`, |
| 66 | + ).toMatch(new RegExp(`/auth/confirm\\?type=${fluxo}`)); |
| 67 | + }); |
| 68 | + |
| 69 | + it(`${fluxo}: o template CONTINUA a query com &token_hash=`, () => { |
| 70 | + const html = ler(template); |
| 71 | + expect(html.length, `${template} veio vazio — o teste ficou cego`).toBeGreaterThan(200); |
| 72 | + |
| 73 | + // A régua é o HREF, não o arquivo: a primeira versão deste teste usava |
| 74 | + // `not.toContain("?token_hash")` sobre o texto inteiro e reprovou por |
| 75 | + // causa do COMENTÁRIO que explica o bug. Medir o arquivo quando se quer |
| 76 | + // medir um atributo é o mesmo erro que o gate do canal zernio cometia. |
| 77 | + const href = /href="(\{\{ \.RedirectTo \}\}[^"]*)"/.exec(html)?.[1]; |
| 78 | + expect(href, `${template}: não achei o href do RedirectTo — o teste ficou cego`).toBeTruthy(); |
| 79 | + expect( |
| 80 | + href, |
| 81 | + `${template} precisa usar & (a query já foi aberta pelo Action); ? aqui duplica o separador e o browser para de enxergar token_hash`, |
| 82 | + ).toBe("{{ .RedirectTo }}&token_hash={{ .TokenHash }}"); |
| 83 | + }); |
| 84 | + } |
| 85 | +}); |
0 commit comments