Files
camilafagundesandClaude Opus 5 ce11a50091 Cobre rotas HTTP, proxy, worker assíncrono e barreiras do dev auth
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>
2026-10-15 20:39:43 -03:00

193 lines
7.0 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createReadyDb, IDS } from "../helpers/world";
import { FakeJar, jar, makeRuntimeDeps, signIn, signOut } from "./helpers";
import type { Db } from "@/infra/db/types";
// Autenticação local assinada: os testes rodam como development.
process.env.ADAPTER_ENV = "development";
process.env.ADAPTER_DEV_AUTH = "1";
process.env.ADAPTER_DEV_AUTH_SECRET = "segredo-de-teste";
let db: Db;
let runtime: ReturnType<typeof makeRuntimeDeps>;
vi.mock("next/headers", () => ({
cookies: async () => jar as unknown as FakeJar,
headers: async () => new Headers(),
}));
vi.mock("@/infra/db/client", () => ({
getDb: async () => db,
probeDb: async () => {
await db.withService((query) => query.query("select 1 as ok"));
},
}));
vi.mock("@/server/spreadsheet-adapter/runtime", () => ({
runtimeDeps: async () => runtime.deps,
}));
beforeAll(async () => {
db = await createReadyDb();
runtime = makeRuntimeDeps(db);
});
beforeEach(() => {
signOut();
});
describe("rotas de saúde", () => {
it("liveness responde sem tocar em dependência", async () => {
const { GET } = await import("@/app/api/health/live/route");
const response = GET();
expect(response.status).toBe(200);
const body = (await response.json()) as { status: string; environment: string };
expect(body.status).toBe("live");
expect(body.environment).toBe("development");
});
it("readiness reporta cada dependência e não devolve connection string", async () => {
const { GET } = await import("@/app/api/health/ready/route");
const response = await GET();
const body = (await response.json()) as {
status: string;
checks: Record<string, string>;
configIssues: string[];
};
expect(body.checks.database).toBe("ok");
expect(body.checks.migrations).toBe("ok");
expect(body.status).toBe("ready");
expect(JSON.stringify(body)).not.toContain("postgres://");
expect(body.configIssues).toEqual([]);
});
});
describe("autenticação das rotas do módulo", () => {
it("recusa sem sessão", async () => {
const { GET } = await import("@/app/api/spreadsheet-adapter/models/route");
const response = await GET();
expect(response.status).toBe(401);
const body = (await response.json()) as { error: { code: string } };
expect(body.error.code).toBe("UNAUTHENTICATED");
});
it("responde com sessão válida no workspace com o módulo ligado", async () => {
signIn(IDS.ana, IDS.alpha);
const { GET } = await import("@/app/api/spreadsheet-adapter/models/route");
const response = await GET();
expect(response.status).toBe(200);
const body = (await response.json()) as { models: unknown[] };
expect(Array.isArray(body.models)).toBe(true);
});
it("recusa a configuração para quem não é owner nem admin", async () => {
signIn(IDS.bruno, IDS.alpha);
const { PATCH } = await import("@/app/api/spreadsheet-adapter/settings/route");
const response = await PATCH(
new Request("http://localhost/api/spreadsheet-adapter/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ allowNonAdminModelUpload: true }),
}),
);
expect(response.status).toBeGreaterThanOrEqual(400);
const body = (await response.json()) as { error: { code: string } };
expect(["FORBIDDEN", "WORKSPACE_FORBIDDEN", "FEATURE_DISABLED"]).toContain(body.error.code);
});
it("bloqueia o módulo desligado no workspace", async () => {
signIn(IDS.diego, IDS.beta);
const { GET } = await import("@/app/api/spreadsheet-adapter/models/route");
const response = await GET();
expect(response.status).toBeGreaterThanOrEqual(400);
});
});
describe("seleção de workspace", () => {
it("lista só as associações reais do usuário", async () => {
signIn(IDS.eva, IDS.beta);
const { GET } = await import("@/app/api/auth/workspace/route");
const response = await GET();
expect(response.status).toBe(200);
const body = (await response.json()) as { memberships: { workspaceId: string }[] };
expect(body.memberships.map((item) => item.workspaceId)).toEqual([IDS.beta]);
});
it("recusa gravar workspace do qual o usuário não é membro", async () => {
signIn(IDS.diego, IDS.beta);
const { POST } = await import("@/app/api/auth/workspace/route");
const response = await POST(
new Request("http://localhost/api/auth/workspace", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workspaceId: IDS.alpha }),
}),
);
expect(response.status).toBe(403);
expect(jar.has("elo_pro_workspace_id")).toBe(false);
});
it("grava a preferência quando a associação existe", async () => {
signIn(IDS.ana, IDS.alpha);
const { POST } = await import("@/app/api/auth/workspace/route");
const response = await POST(
new Request("http://localhost/api/auth/workspace", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workspaceId: IDS.alpha }),
}),
);
expect(response.status).toBe(200);
expect(jar.get("elo_pro_workspace_id")?.value).toBe(IDS.alpha);
});
it("recusa sem sessão", async () => {
const { POST } = await import("@/app/api/auth/workspace/route");
const response = await POST(
new Request("http://localhost/api/auth/workspace", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workspaceId: IDS.alpha }),
}),
);
expect(response.status).toBe(401);
});
});
describe("login de homologação", () => {
it("não existe fora de staging", async () => {
const { POST } = await import("@/app/api/auth/staging/session/route");
const response = await POST(
new Request("http://localhost/api/auth/staging/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "a@b.c", password: "x" }),
}),
);
expect(response.status).toBe(404);
});
});
describe("rate limiting nas rotas", () => {
it("devolve 429 com Retry-After ao estourar a janela", async () => {
signIn(IDS.ana, IDS.alpha);
const { createCoordinator } = await import("@/infra/redis");
const { RATE_LIMITS, windowStart } = await import("@/server/rate-limit");
const { rateLimitKey } = await import("@/infra/redis/redis");
const rule = RATE_LIMITS.read;
const key = rateLimitKey("read", `${IDS.alpha}:${IDS.ana}`, windowStart(rule));
const coordinator = createCoordinator();
for (let index = 0; index < rule.limit; index += 1) {
await coordinator.hit(key, rule.windowSeconds);
}
const { GET } = await import("@/app/api/spreadsheet-adapter/models/route");
const response = await GET();
expect(response.status).toBe(429);
expect(Number(response.headers.get("Retry-After"))).toBeGreaterThan(0);
const body = (await response.json()) as { error: { code: string } };
expect(body.error.code).toBe("RATE_LIMITED");
});
});