Skip to content

Commit 204fffa

Browse files
melgarafaelclaude
andcommitted
fix(e2e): uma fonte de ambiente, em vez de três redações do mesmo segredo
O conserto anterior destravou a suíte (27 specs passaram, contra zero). As 8 que sobraram vermelhas — 6 de system-update no `heartbeat`, o drain do anti-SSRF e o invite-lifecycle — eram todas 401 em rota interna, e a causa era a mesma: `INTERNAL_SECRET` estava declarado em TRÊS lugares com valores diferentes. .env.e2e e2e-placeholder-nao-e-segredo → o servidor env: dos 2 passos ci-placeholder-nao-e-segredo → o processo de teste .env.local do seed ci-placeholder-nao-e-segredo → os seeds MEDIDO (Playwright 1.5x, webServer que imprime o que recebeu): var só no process.env → CHEGA (mescla, não substitui) var nos dois, valores dif. → vence a do `env:` do config Ou seja: o servidor pegava a do arquivo, o teste mandava a do passo, e igualar as strings à mão só adiaria a próxima divergência. O conserto é fonte única — o workflow publica o `.env.e2e` inteiro no `$GITHUB_ENV` e o `.env.local` do seed passa a ser `cp .env.e2e`. Zero segredos redigitados no workflow (era 24 linhas). De brinde, o processo de teste passa a ver as chaves de CIFRA de verdade (32 bytes, geradas pelo script) em vez do rótulo de 21 caracteres que o bloco `env:` fixava — o mesmo que o comentário do script já registrava como causa de `credential_decrypt_failed`. Dois comentários que a medição desmentiu, corrigidos junto: - `playwright.config.ts` avisava "NÃO declare `env:` aqui sem espalhar `process.env` junto ... declarar substitui o ambiente inteiro" — e a linha logo acima declarava `env: envDoE2E()`. O arquivo se contradizia, e a afirmação é falsa: o Playwright mescla. Trocado pela tabela medida. - `gerar-env-e2e.sh` prometia "valores iguais aos do CI" e eles não eram iguais. É a frase que fazia a divergência parecer impossível. A guarda ganhou as duas propriedades novas, sabotadas uma a uma: redigitar `INTERNAL_SECRET` num passo → vermelho; parar de publicar o `.env.e2e` (que faria o caso anterior passar por ausência) → vermelho. gov:verify: 0 erros de lint, 286 arquivos / 2933 testes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HstH7nNmrTvCtZsasppeHj
1 parent 94ab896 commit 204fffa

4 files changed

Lines changed: 120 additions & 45 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ jobs:
9999
- name: Gerar o .env.e2e (ambiente da suíte)
100100
run: pnpm e2e:env
101101

102+
# E publica o arquivo no ambiente do JOB, para que servidor e testes leiam
103+
# literalmente a mesma fonte.
104+
#
105+
# Sem isto havia duas: o `.env.e2e` (que o playwright.config injeta no
106+
# `next start`) e um bloco `env:` copiado à mão em cada passo de teste. As
107+
# duas divergiram — `INTERNAL_SECRET` valia `e2e-placeholder…` no servidor
108+
# e `ci-placeholder…` no processo de teste. Sintoma medido: 401 em toda
109+
# chamada a rota interna, 8 specs vermelhas (6 de system-update no
110+
# heartbeat, o drain do anti-SSRF, e o invite-lifecycle).
111+
#
112+
# Medido no Playwright 1.5x: `webServer.env` MESCLA com o ambiente herdado
113+
# (uma var só do shell chega ao servidor) e VENCE nas colisões. Ou seja,
114+
# copiar valores à mão nunca ia empatar com o arquivo — só uma fonte
115+
# empata.
116+
#
117+
# De brinde, o processo de teste passa a ver as chaves de CIFRA de verdade
118+
# (32 bytes, geradas pelo script) em vez do rótulo de 21 caracteres que o
119+
# bloco `env:` fixava e que o app recusa.
120+
- name: Publicar o .env.e2e no ambiente do job
121+
run: grep -vE '^[[:space:]]*(#|$)' .env.e2e >> "$GITHUB_ENV"
122+
102123
# `pnpm e2e:build`, não `pnpm build`: as três `NEXT_PUBLIC_*` são embutidas
103124
# no BUNDLE durante o build. Buildar com um env e trocar só no `next start`
104125
# deixaria a URL errada dentro do JavaScript que roda no browser — servidor
@@ -111,19 +132,23 @@ jobs:
111132
- name: Instalar o browser
112133
run: pnpm exec playwright install --with-deps chromium
113134

114-
# O seed lê `.env.local` do cwd (não de process.env), então o arquivo é
115-
# escrito aqui. Ele cria 1 org + 4 usuários com os 4 papéis e um TOTP
116-
# verified de secret conhecido no admin — sem isso, todo spec que loga
117-
# fica de fora, que era a maior parte da suíte.
135+
# `.env.local` é CÓPIA do `.env.e2e`, não uma terceira redação dele.
136+
#
137+
# O arquivo precisa existir porque alguns seeds leem `.env.local` do disco,
138+
# não de `process.env`. Ele era montado à mão aqui, com os valores
139+
# redigitados — e foi a terceira fonte do mesmo `INTERNAL_SECRET`, com o
140+
# terceiro valor. Copiar não pode divergir; redigitar já divergiu.
141+
#
142+
# No CI as duas apontam para o mesmo stack local, então a cópia não
143+
# reintroduz o risco que o `.env.e2e` existe para evitar (o `.env.local` de
144+
# um checkout de trabalho apontar para produção).
145+
#
146+
# O seed cria 1 org + 4 usuários com os 4 papéis e um TOTP verified de
147+
# secret conhecido no admin — sem ele, todo spec que loga fica de fora, que
148+
# era a maior parte da suíte.
118149
- name: Semear credenciais de teste
119150
run: |
120-
cat > .env.local <<EOF
121-
NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
122-
NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
123-
SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY
124-
INTERNAL_SECRET=ci-placeholder-nao-e-segredo
125-
SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
126-
EOF
151+
cp .env.e2e .env.local
127152
pnpm exec tsx scripts/seed-e2e-credentials.ts
128153
129154
# `SUPABASE_DB_URL` no `.env.local` (e não só no `env:` do passo de teste):
@@ -195,18 +220,10 @@ jobs:
195220
kanban-owner-filter.spec.ts queue-assign.spec.ts \
196221
risk-radar.spec.ts invite-lifecycle.spec.ts system-update.spec.ts
197222
env:
198-
INTERNAL_SECRET: ci-placeholder-nao-e-segredo
199-
CPF_ENCRYPTION_KEY: ci-placeholder-nao-e-segredo
200-
WAHA_BYO_ENCRYPTION_KEY: ci-placeholder-nao-e-segredo
201-
AI_CRED_AES_KEY: ci-placeholder-nao-e-segredo
202-
SUPABASE_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
203-
WAHA_API_BASE_URL: http://127.0.0.1:3999
204-
WAHA_API_KEY: ci-placeholder-nao-e-segredo
205-
WAHA_WEBHOOK_BASE_URL: http://127.0.0.1:3001
206-
UPSTASH_REDIS_REST_URL: http://127.0.0.1:3998
207-
UPSTASH_REDIS_REST_TOKEN: ci-placeholder-nao-e-segredo
208-
# CI = 1 IP para todos os specs; o teto de produção (60/5min) é irreal aqui.
209-
# O teto por CONTA (5 falhas) NÃO muda — é ele que barra brute force.
223+
# Só o que NÃO vem do `.env.e2e`: isto é ajuste de CI, não config do
224+
# produto. CI = 1 IP para todos os specs, e o teto de produção
225+
# (60/5min) é irreal aqui. O teto por CONTA (5 falhas) NÃO muda — é
226+
# ele que barra brute force.
210227
AUTH_RATE_LIMIT_LOGIN_IP: "1000"
211228

212229
- name: E2E — parte 2 de 2 (processo novo, contador de login zerado)
@@ -222,18 +239,10 @@ jobs:
222239
webhooks.spec.ts \
223240
capacidades-do-agente.spec.ts
224241
env:
225-
INTERNAL_SECRET: ci-placeholder-nao-e-segredo
226-
CPF_ENCRYPTION_KEY: ci-placeholder-nao-e-segredo
227-
WAHA_BYO_ENCRYPTION_KEY: ci-placeholder-nao-e-segredo
228-
AI_CRED_AES_KEY: ci-placeholder-nao-e-segredo
229-
SUPABASE_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
230-
WAHA_API_BASE_URL: http://127.0.0.1:3999
231-
WAHA_API_KEY: ci-placeholder-nao-e-segredo
232-
WAHA_WEBHOOK_BASE_URL: http://127.0.0.1:3001
233-
UPSTASH_REDIS_REST_URL: http://127.0.0.1:3998
234-
UPSTASH_REDIS_REST_TOKEN: ci-placeholder-nao-e-segredo
235-
# CI = 1 IP para todos os specs; o teto de produção (60/5min) é irreal aqui.
236-
# O teto por CONTA (5 falhas) NÃO muda — é ele que barra brute force.
242+
# Só o que NÃO vem do `.env.e2e`: isto é ajuste de CI, não config do
243+
# produto. CI = 1 IP para todos os specs, e o teto de produção
244+
# (60/5min) é irreal aqui. O teto por CONTA (5 falhas) NÃO muda — é
245+
# ele que barra brute force.
237246
AUTH_RATE_LIMIT_LOGIN_IP: "1000"
238247

239248
- name: Declarar o que este job ainda NÃO cobre

playwright.config.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,22 @@ export default defineConfig({
8686
// false: reusar um server que já ocupa a porta pode ser OUTRO processo
8787
// (ex.: bundle do Remotion na 3000) — o teste precisa do NOSSO next start.
8888
reuseExistingServer: false,
89-
// NÃO declare `env:` aqui sem espalhar `process.env` junto. O default do
90-
// Playwright é herdar o ambiente, e o CI depende disso: o teto de login por
91-
// IP (`AUTH_RATE_LIMIT_LOGIN_IP`) é definido no passo do workflow e precisa
92-
// chegar ao `next start`, porque quem aplica o rate limit é o SERVIDOR.
93-
// Declarar `env` aqui substitui o ambiente inteiro e o teto volta a 60 —
94-
// o sintoma seria "Muitas tentativas" numa spec tardia, não um erro de config.
89+
// Sobre a precedência de `env`, MEDIDO (Playwright 1.5x, 2026-08-07) com um
90+
// webServer que imprime o que recebeu:
91+
//
92+
// var só no process.env → CHEGA ao servidor (mescla, não substitui)
93+
// var só no `env:` do config → chega
94+
// var nos DOIS, valores dif. → vence a do `env:` do config
95+
//
96+
// A primeira linha é o que mantém `AUTH_RATE_LIMIT_LOGIN_IP` funcionando:
97+
// ele é definido no passo do workflow e quem aplica o teto é o SERVIDOR.
98+
//
99+
// A terceira é a armadilha. Uma chave que exista no `.env.e2e` E no ambiente
100+
// do CI silenciosamente resolve para valores DIFERENTES nos dois lados —
101+
// servidor com um, processo de teste com outro. Foi assim que
102+
// `INTERNAL_SECRET` derrubou 8 specs com 401. Por isso o workflow publica o
103+
// `.env.e2e` inteiro no ambiente do job em vez de redigitar valores: uma
104+
// fonte não colide consigo mesma.
95105
timeout: 120_000,
96106
},
97107
projects: [{ name: "chromium", use: { browserName: "chromium" } }],

scripts/gerar-env-e2e.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
8787
8888
# Placeholders: 'next start' roda em NODE_ENV=production, e lib/env.ts exige
8989
# estas vars em produção. As specs não exercitam os serviços por trás delas.
90-
# Valores iguais aos do CI (.github/workflows/e2e.yml), para que local e CI
91-
# falhem pelos mesmos motivos.
90+
# Local e CI falham pelos mesmos motivos porque leem ESTE arquivo: o workflow
91+
# publica o `.env.e2e` no ambiente do job em vez de redigitar os valores. A
92+
# versão anterior desta linha prometia "valores iguais aos do CI" e eles não
93+
# eram iguais (`e2e-placeholder…` aqui, `ci-placeholder…` lá) — a promessa por
94+
# coincidência durou até a primeira divergência, que custou 8 specs em 401.
9295
INTERNAL_SECRET=e2e-placeholder-nao-e-segredo
9396
# As três abaixo são chaves de CIFRA de verdade: o app exige 32 bytes e recusa
9497
# um rótulo. Medido — com o placeholder, criar credencial de IA devolvia 500

tests/unit/e2e-workflow-honra-o-env.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@
2424
*
2525
* ## O que se guarda
2626
*
27-
* A ORDEM, não a presença: existir um passo que gera o `.env.e2e` não adianta
28-
* se ele vier depois de quem consome. Por isso a asserção é sobre índices.
27+
* Duas propriedades, e elas falham por motivos diferentes.
28+
*
29+
* **A ordem**, não a presença: existir um passo que gera o `.env.e2e` não
30+
* adianta se ele vier depois de quem consome. Por isso a asserção é sobre
31+
* índices.
32+
*
33+
* **A fonte única**, não a igualdade: o arquivo chegava ao servidor (via
34+
* `playwright.config`) e um bloco `env:` copiado à mão chegava ao processo de
35+
* teste. Medido no Playwright 1.5x — em colisão o `env:` do config vence — os
36+
* dois lados resolviam `INTERNAL_SECRET` para valores diferentes e toda rota
37+
* interna devolvia 401. Foram 8 specs, no run seguinte ao conserto da ordem: o
38+
* mesmo refactor, a terceira ponta solta. Igualar as strings à mão consertaria
39+
* o dia; publicar o arquivo consertou a classe.
2940
*/
3041
import { describe, expect, it } from "vitest";
3142
import * as fs from "node:fs";
@@ -89,6 +100,48 @@ describe("o workflow do e2e honra o contrato de ambiente que a suíte exige", ()
89100
).toBeLessThan(consome);
90101
});
91102

103+
it("nenhum segredo do produto é redigitado no workflow", () => {
104+
// O defeito que este caso guarda é DIFERENTE da ordem: o `.env.e2e` chegava
105+
// ao servidor (via playwright.config) e um bloco `env:` copiado à mão
106+
// chegava ao processo de teste. Medido no Playwright 1.5x: em colisão, o
107+
// `env:` do config VENCE — então os dois lados resolviam `INTERNAL_SECRET`
108+
// para valores diferentes e toda rota interna devolvia 401. Oito specs.
109+
//
110+
// A regra não é "não repita": é que a lista abaixo tem UMA fonte, o
111+
// `.env.e2e`, e o workflow a publica em vez de reescrevê-la.
112+
const DO_ENV_E2E = [
113+
"INTERNAL_SECRET",
114+
"CPF_ENCRYPTION_KEY",
115+
"WAHA_BYO_ENCRYPTION_KEY",
116+
"AI_CRED_AES_KEY",
117+
"WAHA_API_KEY",
118+
"UPSTASH_REDIS_REST_TOKEN",
119+
"SUPABASE_DB_URL",
120+
];
121+
122+
// Só linhas de conteúdo: um comentário que MENCIONA a variável (e vários
123+
// mencionam, é onde a história está escrita) não é uma segunda fonte.
124+
const atribuicoes = workflow
125+
.split("\n")
126+
.filter((l) => !l.trim().startsWith("#"))
127+
.filter((l) => DO_ENV_E2E.some((k) => new RegExp(`(^|\\s)${k}\\s*[:=]`).test(l)));
128+
129+
expect(
130+
atribuicoes,
131+
"estas variáveis vêm do .env.e2e; redigitá-las cria uma segunda fonte que já divergiu uma vez",
132+
).toEqual([]);
133+
});
134+
135+
it("o workflow publica o .env.e2e no ambiente do job (a fonte única de fato chega)", () => {
136+
// Sem este caso, o anterior passaria trivialmente num workflow que
137+
// simplesmente não define nada — verde por ausência, e a suíte sem segredo
138+
// nenhum. É a guarda de vacuidade do par.
139+
const publica = COMANDOS.some((l) => /\.env\.e2e.*GITHUB_ENV/.test(l));
140+
expect(publica, "ninguém publica o .env.e2e — o processo de teste fica sem os segredos").toBe(
141+
true,
142+
);
143+
});
144+
92145
it("o build do job é o que embute as NEXT_PUBLIC_* do ambiente de teste", () => {
93146
// `pnpm build` puro deixaria a URL do `.env.local` dentro do bundle do
94147
// browser: servidor falando com um banco e cliente com outro, no mesmo

0 commit comments

Comments
 (0)