Os 34 testes existentes batiam só na camada de serviço: as rotas, o proxy e o loop do worker não tinham cobertura alguma. A suíte vai de 34 para 102. - `tests/http/`: rotas reais com o caminho real de autenticação, sem subir servidor. Cobre 401 sem sessão, módulo desligado, papel sem permissão, 403 ao tentar gravar workspace de terceiro, e 429 com Retry-After. - `tests/http/proxy.test.ts`: o proxy não propaga query da requisição para o login, o que impediria um returnTo forjado. - `tests/integration/worker.test.ts`: jornada assíncrona pelo loop real. A web enfileira e devolve `queued`, o download falha antes do tick, o tick conclui e os três formatos saem. Também: job sobrevive à perda da fila do Redis, worker relê o contexto e não processa com a flag desligada, TTL expira temporários sem tocar o modelo, e desligamento gracioso. - `tests/unit/dev-auth-guard.test.ts`: matriz de ambientes das três barreiras do login local. Um token de dev válido é recusado quando o ambiente é staging. Espaço em volta de um valor de ADAPTER_ENV é tolerado de propósito, e isso está declarado no teste; valor desconhecido nunca vira development. - `tests/services/`: Postgres e Redis reais. Confere `app_user` sem BYPASSRLS, FORCE RLS em todas as tabelas, isolamento entre workspaces, auditoria não alterável pelo papel da aplicação, e lock entre conexões diferentes. Sem as variáveis de serviço os casos são IGNORADOS, nunca aprovados, e ficam fora de `npm test` para a suíte padrão passar em qualquer máquina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
7.7 KiB
TypeScript
188 lines
7.7 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { adapterEnv, collectConfigIssues, stagingAuthEnabled } from "@/config/env";
|
|
import { devAuthEnabled } from "@/server/auth/dev-session";
|
|
|
|
/**
|
|
* O login local sem senha não pode existir em homologação pública, em nenhuma
|
|
* combinação de variáveis. Há três barreiras independentes e esta suíte cobre
|
|
* as três, na matriz inteira de ambientes.
|
|
*/
|
|
|
|
const original = {
|
|
ADAPTER_ENV: process.env.ADAPTER_ENV,
|
|
NODE_ENV: process.env.NODE_ENV,
|
|
ADAPTER_DEV_AUTH: process.env.ADAPTER_DEV_AUTH,
|
|
ADAPTER_DEV_AUTH_SECRET: process.env.ADAPTER_DEV_AUTH_SECRET,
|
|
};
|
|
|
|
function setEnv(values: Record<string, string | undefined>): void {
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
setEnv(original);
|
|
});
|
|
|
|
describe("barreira 1: devAuthEnabled", () => {
|
|
const casos: { nome: string; env: Record<string, string | undefined>; esperado: boolean }[] = [
|
|
{
|
|
nome: "development com flag e segredo",
|
|
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: true,
|
|
},
|
|
{
|
|
nome: "development sem a flag",
|
|
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "0", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "development sem segredo",
|
|
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: undefined },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "staging com flag e segredo",
|
|
env: { ADAPTER_ENV: "staging", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "production com flag e segredo",
|
|
env: { ADAPTER_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "ADAPTER_ENV ausente com NODE_ENV=production",
|
|
env: { ADAPTER_ENV: undefined, NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "ADAPTER_ENV com valor inválido",
|
|
env: { ADAPTER_ENV: "homolog", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "ADAPTER_ENV com maiúsculas não é aceito como development",
|
|
env: { ADAPTER_ENV: "Development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
{
|
|
nome: "staging mesmo com NODE_ENV=development",
|
|
env: { ADAPTER_ENV: "staging", NODE_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
|
esperado: false,
|
|
},
|
|
];
|
|
|
|
for (const caso of casos) {
|
|
it(`${caso.nome} → ${caso.esperado ? "ligado" : "desligado"}`, () => {
|
|
setEnv({ ADAPTER_ENV: undefined, NODE_ENV: "test", ADAPTER_DEV_AUTH: undefined, ADAPTER_DEV_AUTH_SECRET: undefined });
|
|
setEnv(caso.env);
|
|
expect(devAuthEnabled()).toBe(caso.esperado);
|
|
});
|
|
}
|
|
|
|
it("um valor inesperado de ADAPTER_ENV nunca resolve para development", () => {
|
|
for (const valor of ["prod", "hml", "homolog", "homologacao", "stage", "staging ", "DEV", "Development", "dev"]) {
|
|
setEnv({ ADAPTER_ENV: valor, NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
|
expect(adapterEnv(), `valor ${JSON.stringify(valor)}`).not.toBe("development");
|
|
expect(devAuthEnabled(), `valor ${JSON.stringify(valor)}`).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("espaço em volta de um valor válido é tolerado, de propósito", () => {
|
|
// Espaço sobrando em arquivo de env é erro de digitação comum, e o valor
|
|
// ainda é inequívoco. O que nunca acontece é um valor DESCONHECIDO virar
|
|
// development: o caso acima cobre isso, e `ADAPTER_ENV` vazio cai em
|
|
// NODE_ENV. Reconhecer " staging " como staging é o comportamento seguro.
|
|
setEnv({ ADAPTER_ENV: " staging ", NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
|
expect(adapterEnv()).toBe("staging");
|
|
expect(devAuthEnabled()).toBe(false);
|
|
|
|
setEnv({ ADAPTER_ENV: " development ", NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
|
expect(adapterEnv()).toBe("development");
|
|
expect(devAuthEnabled()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("barreira 2: o start é recusado", () => {
|
|
const base = {
|
|
ADAPTER_ENV: "staging",
|
|
NEXT_PUBLIC_SUPABASE_URL: "https://p.supabase.co",
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: "anon",
|
|
SUPABASE_SERVICE_ROLE_KEY: "service",
|
|
SUPABASE_STORAGE_BUCKET: "bucket-hml",
|
|
DATABASE_URL: "postgres://host/db",
|
|
REDIS_URL: "redis://host:6379",
|
|
SPREADSHEET_TEMP_TTL_SECONDS: "86400",
|
|
ADAPTER_JOB_MODE: "worker",
|
|
ADAPTER_APPLY_IDENTITY_SHIM: "1",
|
|
ADAPTER_STAGING_AUTH: "1",
|
|
};
|
|
|
|
it("staging com ADAPTER_DEV_AUTH=1 não passa na validação", () => {
|
|
const issues = collectConfigIssues({ ...base, ADAPTER_DEV_AUTH: "1" });
|
|
expect(issues.map((issue) => issue.variable)).toContain("ADAPTER_DEV_AUTH");
|
|
});
|
|
|
|
it("production com ADAPTER_DEV_AUTH=1 não passa na validação", () => {
|
|
const issues = collectConfigIssues({
|
|
...base,
|
|
ADAPTER_ENV: "production",
|
|
ADAPTER_STAGING_AUTH: "0",
|
|
SENTRY_DSN: "https://k@sentry.io/1",
|
|
ELO_PRO_LOGIN_URL: "https://login.elo.example",
|
|
ADAPTER_DEV_AUTH: "1",
|
|
});
|
|
expect(issues.map((issue) => issue.variable)).toContain("ADAPTER_DEV_AUTH");
|
|
});
|
|
|
|
it("staging sem a flag de dev passa", () => {
|
|
expect(collectConfigIssues({ ...base, ADAPTER_DEV_AUTH: "0" })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("barreira 3: o provedor não entra no caminho de código", () => {
|
|
it("fora de development só o Supabase resolve identidade", async () => {
|
|
setEnv({ ADAPTER_ENV: "staging", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
|
const { activeProviders } = await import("@/server/auth/identity-provider");
|
|
expect(activeProviders().map((provider) => provider.name)).toEqual(["supabase"]);
|
|
});
|
|
|
|
it("em development o provedor local vem primeiro", async () => {
|
|
setEnv({ ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
|
const { activeProviders } = await import("@/server/auth/identity-provider");
|
|
expect(activeProviders().map((provider) => provider.name)).toEqual(["dev", "supabase"]);
|
|
});
|
|
|
|
it("o provedor local recusa o token mesmo se for chamado em staging", async () => {
|
|
setEnv({ ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "segredo" });
|
|
const { signDevSession } = await import("@/server/auth/dev-session");
|
|
const token = signDevSession("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "11111111-1111-4111-8111-111111111111");
|
|
|
|
setEnv({ ADAPTER_ENV: "staging" });
|
|
const { devIdentityProvider } = await import("@/server/auth/identity-provider");
|
|
const jar = {
|
|
get: (name: string) => (name === "adapter_dev_session" ? { name, value: token } : undefined),
|
|
getAll: () => [{ name: "adapter_dev_session", value: token }],
|
|
set: () => undefined,
|
|
};
|
|
expect(await devIdentityProvider.resolve(jar as never)).toBeNull();
|
|
expect(devIdentityProvider.hadSessionHint(jar as never)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("login de homologação não vaza para produção", () => {
|
|
it("stagingAuthEnabled é falso em development e production", () => {
|
|
const base = {
|
|
ADAPTER_STAGING_AUTH: "1",
|
|
NEXT_PUBLIC_SUPABASE_URL: "https://p.supabase.co",
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: "anon",
|
|
};
|
|
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "staging" })).toBe(true);
|
|
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "production" })).toBe(false);
|
|
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "development" })).toBe(false);
|
|
});
|
|
});
|