import { runScript } from "./_bootstrap"; /** * Smoke de homologação contra o ambiente em pé. * * Fase 1 (sempre): saúde, recusa sem sessão, cabeçalhos, ausência de dev auth. * Fase 2 (só com credencial fornecida): login real no Supabase Auth de * homologação, resolução de workspace e leitura autenticada do módulo. * * Nenhuma credencial é inventada aqui. A fase 2 só roda quando a operação * fornece as variáveis abaixo, com uma conta criada no painel do Supabase: * * ADAPTER_SMOKE_URL URL pública, ex. https://hml-ada.elopro.app * ADAPTER_SMOKE_EMAIL e-mail de uma conta de teste de homologação * ADAPTER_SMOKE_PASSWORD senha dessa conta * ADAPTER_SMOKE_WORKSPACE (opcional) UUID do workspace esperado * * Sem ADAPTER_SMOKE_EMAIL/PASSWORD a fase 2 é reportada como PENDENTE, nunca * como aprovada. O envio de planilha, o processamento e o download seguem no * roteiro manual do runbook: exigem arquivo e a flag do módulo ligada. */ type Check = { name: string; ok: boolean; detail: string; pending?: boolean }; const base = (process.env.ADAPTER_SMOKE_URL ?? "").replace(/\/$/, ""); const email = process.env.ADAPTER_SMOKE_EMAIL; const password = process.env.ADAPTER_SMOKE_PASSWORD; const expectedWorkspace = process.env.ADAPTER_SMOKE_WORKSPACE; if (!base) { console.error("Defina ADAPTER_SMOKE_URL com a URL pública do ambiente."); process.exit(1); } const checks: Check[] = []; function record(name: string, ok: boolean, detail: string) { checks.push({ name, ok, detail }); console.log(`${ok ? "PASS" : "FAIL"} ${name} — ${detail}`); } function pending(name: string, detail: string) { checks.push({ name, ok: false, detail, pending: true }); console.log(`PENDENTE ${name} — ${detail}`); } async function fetchJson(path: string, init?: RequestInit): Promise<{ status: number; body: unknown; response: Response }> { const response = await fetch(`${base}${path}`, { redirect: "manual", ...init, headers: { accept: "application/json", ...(init?.headers ?? {}) }, }); const text = await response.text(); let body: unknown = text; try { body = JSON.parse(text); } catch { /* resposta não-JSON entra como texto */ } return { status: response.status, body, response }; } /** Junta os cookies de Set-Cookie para a próxima requisição. */ function collectCookies(response: Response, jar: Map): void { const raw = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : []; for (const line of raw) { const [pair] = line.split(";"); const index = pair.indexOf("="); if (index > 0) jar.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim()); } } function cookieHeader(jar: Map): string { return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; "); } runScript("smoke-staging", async () => { // ---------------------------------------------------------------- fase 1 console.log(`== fase 1: ambiente em ${base} ==`); try { const live = await fetchJson("/api/health/live"); const body = live.body as { status?: string }; record("health/live", live.status === 200 && body?.status === "live", `status ${live.status}`); } catch (error) { record("health/live", false, error instanceof Error ? error.message : "falha de rede"); } let environment: string | undefined; try { const ready = await fetchJson("/api/health/ready"); const body = ready.body as { status?: string; environment?: string; checks?: Record; reasons?: Record; configIssues?: string[]; }; environment = body?.environment; record( "health/ready", ready.status === 200 && body?.status === "ready", `status ${ready.status} checks=${JSON.stringify(body?.checks ?? {})} reasons=${JSON.stringify(body?.reasons ?? {})}`, ); record("ambiente é staging", body?.environment === "staging", `environment=${body?.environment ?? "?"}`); record( "sem pendência de configuração", (body?.configIssues ?? []).length === 0, (body?.configIssues ?? []).join("; ") || "nenhuma", ); } catch (error) { record("health/ready", false, error instanceof Error ? error.message : "falha de rede"); } try { const anon = await fetchJson("/api/spreadsheet-adapter/models"); record("API sem sessão recusa", anon.status === 401, `status ${anon.status}`); } catch (error) { record("API sem sessão recusa", false, error instanceof Error ? error.message : "falha de rede"); } try { const page = await fetch(`${base}/adaptador`, { redirect: "manual" }); const location = page.headers.get("location") ?? ""; record( "/adaptador sem sessão redireciona ao login", page.status >= 300 && page.status < 400 && location.includes("/login"), `status ${page.status} location=${location || "(vazio)"}`, ); } catch (error) { record("/adaptador sem sessão redireciona ao login", false, error instanceof Error ? error.message : "falha de rede"); } try { const response = await fetch(`${base}/api/health/live`, { redirect: "manual" }); const required = [ "content-security-policy", "strict-transport-security", "x-content-type-options", "x-frame-options", "referrer-policy", "permissions-policy", ]; const missing = required.filter((header) => !response.headers.get(header)); record("cabeçalhos de segurança", missing.length === 0, missing.length ? `faltam: ${missing.join(", ")}` : "todos presentes"); } catch (error) { record("cabeçalhos de segurança", false, error instanceof Error ? error.message : "falha de rede"); } // Dev auth não pode existir em homologação pública, em nenhuma circunstância. try { const dev = await fetch(`${base}/api/dev/session`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId: "00000000-0000-4000-8000-000000000000", workspaceId: "00000000-0000-4000-8000-000000000000", }), }); record("dev auth indisponível", dev.status === 404, `status ${dev.status}`); } catch (error) { record("dev auth indisponível", false, error instanceof Error ? error.message : "falha de rede"); } // ---------------------------------------------------------------- fase 2 console.log(""); console.log("== fase 2: login real no Supabase Auth de homologação =="); if (!email || !password) { pending( "login real", "defina ADAPTER_SMOKE_EMAIL e ADAPTER_SMOKE_PASSWORD de uma conta criada no painel do Supabase de homologação", ); pending("resolução de workspace autenticada", "depende do login real"); pending("leitura autenticada do módulo", "depende do login real"); } else { const jar = new Map(); let logged = false; try { const login = await fetch(`${base}/api/auth/staging/session`, { method: "POST", redirect: "manual", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); collectCookies(login, jar); logged = login.status === 200 && jar.size > 0; record("login real", logged, `status ${login.status} cookies=${jar.size}`); } catch (error) { record("login real", false, error instanceof Error ? error.message : "falha de rede"); } if (logged) { try { const workspaces = await fetchJson("/api/auth/workspace", { headers: { cookie: cookieHeader(jar) } }); const body = workspaces.body as { memberships?: { workspaceId: string; workspaceName: string; role: string }[] }; const list = body?.memberships ?? []; record( "resolução de workspace autenticada", workspaces.status === 200 && list.length > 0, `status ${workspaces.status} associações=${list.length}`, ); if (expectedWorkspace) { record( "workspace esperado está associado", list.some((item) => item.workspaceId.toLowerCase() === expectedWorkspace.toLowerCase()), `esperado=${expectedWorkspace}`, ); } if (list.length > 0) { const chosen = expectedWorkspace ?? list[0].workspaceId; const select = await fetch(`${base}/api/auth/workspace`, { method: "POST", redirect: "manual", headers: { "Content-Type": "application/json", cookie: cookieHeader(jar) }, body: JSON.stringify({ workspaceId: chosen }), }); collectCookies(select, jar); record("seleção de workspace aceita", select.status === 200, `status ${select.status}`); } } catch (error) { record("resolução de workspace autenticada", false, error instanceof Error ? error.message : "falha de rede"); } try { const models = await fetchJson("/api/spreadsheet-adapter/models", { headers: { cookie: cookieHeader(jar) } }); // 200 com o módulo ligado; 403 FEATURE_DISABLED é resposta correta // enquanto a feature flag do workspace continua desligada. const body = models.body as { error?: { code?: string } }; const aceitavel = models.status === 200 || body?.error?.code === "FEATURE_DISABLED"; record( "leitura autenticada do módulo", aceitavel, `status ${models.status} code=${body?.error?.code ?? "ok"}`, ); } catch (error) { record("leitura autenticada do módulo", false, error instanceof Error ? error.message : "falha de rede"); } } else { pending("resolução de workspace autenticada", "login não concluiu"); pending("leitura autenticada do módulo", "login não concluiu"); } } // ---------------------------------------------------------------- resumo const failed = checks.filter((check) => !check.ok && !check.pending); const pendentes = checks.filter((check) => check.pending); console.log(""); console.log(`${checks.length - failed.length - pendentes.length}/${checks.length} aprovadas, ${pendentes.length} pendentes, ${failed.length} falhas.`); console.log(`ambiente reportado: ${environment ?? "desconhecido"}`); console.log(""); console.log("Continua fora deste script e obrigatório no runbook:"); console.log(" - envio de modelo e de fonte pela interface"); console.log(" - processamento assíncrono com o worker e download XLSX, CSV e PDF"); console.log(" - troca de workspace e sessão expirada no navegador"); console.log(" - evento controlado no Sentry, confirmado sem conteúdo de célula"); console.log(" - ensaio de rollback de imagem"); if (failed.length > 0) process.exit(1); });