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

111 lines
3.9 KiB
TypeScript

import { afterAll, describe, expect, it } from "vitest";
import { RedisCoordinator, lockKey, progressKey, queueKey, rateLimitKey } from "@/infra/redis/redis";
/**
* Lock, fila, progresso e contador contra Redis real. A suíte padrão usa o
* coordenador em memória, que não atravessa processos — é justamente o que o
* lock precisa fazer em homologação com web e worker separados.
*/
const url = process.env.ADAPTER_TEST_REDIS_URL;
const enabled = Boolean(url);
const coordinators: RedisCoordinator[] = [];
function coordinator(): RedisCoordinator {
const instance = new RedisCoordinator(url as string);
coordinators.push(instance);
return instance;
}
afterAll(async () => {
for (const instance of coordinators) await instance.close().catch(() => undefined);
});
describe.skipIf(!enabled)("Redis real", () => {
it("responde ao ping", async () => {
expect(await coordinator().ping()).toBe(true);
});
it("dá o lock a um dono só, entre conexões diferentes", async () => {
const key = `${lockKey("teste-" + Date.now())}`;
const web = coordinator();
const worker = coordinator();
expect(await web.acquireLock(key, "web", 30)).toBe(true);
expect(await worker.acquireLock(key, "worker", 30)).toBe(false);
expect(await worker.ownsLock(key, "web")).toBe(true);
await web.releaseLock(key, "web");
expect(await worker.acquireLock(key, "worker", 30)).toBe(true);
await worker.releaseLock(key, "worker");
});
it("não deixa quem não é dono liberar o lock", async () => {
const key = `${lockKey("dono-" + Date.now())}`;
const web = coordinator();
expect(await web.acquireLock(key, "web", 30)).toBe(true);
await web.releaseLock(key, "intruso");
expect(await web.ownsLock(key, "web")).toBe(true);
await web.releaseLock(key, "web");
});
it("entrega o job enfileirado ao outro processo", async () => {
const jobId = `job-${Date.now()}`;
const web = coordinator();
const worker = coordinator();
await web.enqueue(jobId);
const dequeued = await worker.dequeue();
expect(dequeued).toBe(jobId);
});
it("compartilha progresso entre processos", async () => {
const jobId = `progresso-${Date.now()}`;
const web = coordinator();
const worker = coordinator();
await worker.setProgress(jobId, {
phase: "processing",
label: "Processando",
percent: 42,
sourceIndex: 0,
sourceCount: 1,
});
const progress = await web.getProgress(jobId);
expect(progress?.percent).toBe(42);
});
it("conta a janela de rate limit e expira a chave", async () => {
const key = rateLimitKey("process", `teste-${Date.now()}`, 1);
const instance = coordinator();
expect(await instance.hit(key, 60)).toBe(1);
expect(await instance.hit(key, 60)).toBe(2);
expect(await instance.hit(key, 60)).toBe(3);
});
it("separa as chaves por aplicativo e ambiente", () => {
const previous = process.env.ADAPTER_ENV;
try {
process.env.ADAPTER_ENV = "staging";
const stagingQueue = queueKey();
const stagingProgress = progressKey("x");
process.env.ADAPTER_ENV = "production";
expect(queueKey()).not.toBe(stagingQueue);
expect(progressKey("x")).not.toBe(stagingProgress);
expect(stagingQueue).toContain("adaptador-planilhas:staging");
} finally {
if (previous === undefined) delete process.env.ADAPTER_ENV;
else process.env.ADAPTER_ENV = previous;
}
});
it("recusa com erro de domínio quando o Redis está inacessível", async () => {
const broken = new RedisCoordinator("redis://127.0.0.1:1");
await expect(broken.enqueue("x")).rejects.toMatchObject({ code: "REDIS_UNAVAILABLE" });
expect(await broken.ping()).toBe(false);
await broken.close().catch(() => undefined);
});
});
describe.skipIf(enabled)("Redis real", () => {
it("PENDENTE: defina ADAPTER_TEST_REDIS_URL para executar", () => {
expect(enabled).toBe(false);
});
});