test(security): make Cloudflare block tests deterministic under CI Redis
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m39s
CI / tests-unit (push) Successful in 1m42s
CI / tests-ui (push) Successful in 2m31s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m1s

cloudflare-api unit tests drove the real Redis connection when REDIS_URL was set (CI), causing cross-test bleed. Mock @/lib/redis with an in-memory fake identical to the gate integration test.
This commit is contained in:
openhands committed 2026-09-22 23:31:41 +02:00
1 parent 4479753160
commit 6264f9fb20
1 file changed
+54
+54
View File
@@ -10,6 +10,58 @@ import {
verifyCloudflareConnection, verifyCloudflareConnection,
} from "./cloudflare-api"; } from "./cloudflare-api";
// The coordination state must be deterministic in CI too (where REDIS_URL is
// set), so the unit test drives a small in-memory Redis fake.
const state = vi.hoisted(() => ({ map: new Map<string, string>() }));
vi.mock("@/lib/redis", () => ({
redis: {
get: async (key: string) => state.map.get(key) ?? null,
set: async (
key: string,
value: string,
_mode?: string,
_seconds?: number,
nx?: string,
) => {
if (nx === "NX" && state.map.has(key)) return null;
state.map.set(key, value);
return "OK";
},
del: async (...keys: string[]) => {
for (const key of keys) state.map.delete(key);
return keys.length;
},
incr: async (key: string) => {
const next = (Number(state.map.get(key)) || 0) + 1;
state.map.set(key, String(next));
return next;
},
pexpire: async () => 1,
pttl: async () => 60_000,
sadd: async (key: string, member: string) => {
const members = new Set(
(state.map.get(key) ?? "").split("\u0001").filter(Boolean),
);
members.add(member);
state.map.set(key, [...members].join("\u0001"));
return 1;
},
srem: async (key: string, member: string) => {
const members = new Set(
(state.map.get(key) ?? "").split("\u0001").filter(Boolean),
);
const before = members.size;
members.delete(member);
state.map.set(key, [...members].join("\u0001"));
return before - members.size;
},
smembers: async (key: string) =>
(state.map.get(key) ?? "").split("\u0001").filter(Boolean),
},
__esModule: true,
}));
function jsonResponse(body: unknown, status = 200): Response { function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), { return new Response(JSON.stringify(body), {
status, status,
@@ -28,6 +80,7 @@ describe("cloudflare-api", () => {
beforeEach(() => { beforeEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
vi.unstubAllEnvs(); vi.unstubAllEnvs();
state.map.clear();
resetCloudflareAutoBlockCache(); resetCloudflareAutoBlockCache();
fetchMock = vi.fn(); fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
@@ -36,6 +89,7 @@ describe("cloudflare-api", () => {
afterEach(() => { afterEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
vi.unstubAllEnvs(); vi.unstubAllEnvs();
state.map.clear();
resetCloudflareAutoBlockCache(); resetCloudflareAutoBlockCache();
}); });