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); }); });