// Tests unitaires de backend/src/utils/Limiter.js (node limiter.test.js) const assert = require("assert"); const path = require("path").resolve(__dirname, "../../src/utils/Limiter.js"); const { createLimiter, mapLimit, createInflight, createCache } = require(path); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const tests = []; const test = (name, fn) => tests.push({ name, fn }); test("limiter: concurrence max respectée et résultats corrects", async () => { const lim = createLimiter(2); let active = 0, peak = 0; const res = await Promise.all([1, 2, 3, 4, 5, 6].map(i => lim.run(async () => { active++; peak = Math.max(peak, active); await sleep(20); active--; return i * 10; }))); assert.deepStrictEqual(res, [10, 20, 30, 40, 50, 60]); assert.strictEqual(peak, 2); assert.strictEqual(lim.active, 0); assert.strictEqual(lim.pending, 0); }); test("limiter: erreurs (async et sync) propagées sans bloquer la file", async () => { const lim = createLimiter(1); await assert.rejects(lim.run(async () => { throw new Error("a"); }), /a/); await assert.rejects(lim.run(() => { throw new Error("b"); }), /b/); assert.strictEqual(await lim.run(() => 42), 42); }); test("limiter: minIntervalMs espace les démarrages", async () => { const lim = createLimiter(5, { minIntervalMs: 30 }); const starts = []; await Promise.all([0, 1, 2, 3].map(() => lim.run(async () => { starts.push(Date.now()); }))); for (let i = 1; i < starts.length; i++) assert.ok(starts[i] - starts[i - 1] >= 25, "écart " + (starts[i] - starts[i - 1])); }); test("mapLimit: ordre conservé, concurrence bornée, erreurs ignorées", async () => { let active = 0, peak = 0; const out = await mapLimit([30, 5, 20, 1, 10], 3, async (ms, i) => { active++; peak = Math.max(peak, active); await sleep(ms); active--; if (i === 2) throw new Error("x"); return ms; }, () => null); assert.deepStrictEqual(out, [30, 5, null, 1, 10]); assert.strictEqual(peak, 3); assert.deepStrictEqual(await mapLimit(null, 2, async x => x), []); }); test("inflight: appels simultanés dédupliqués, libérés ensuite", async () => { const inf = createInflight(); let calls = 0; const fn = async () => { calls++; await sleep(10); return "v"; }; const [a, b] = await Promise.all([inf.run("k", fn), inf.run("k", fn)]); assert.strictEqual(a, "v"); assert.strictEqual(b, "v"); assert.strictEqual(calls, 1); assert.strictEqual(inf.size, 0); await inf.run("k", fn); assert.strictEqual(calls, 2); await assert.rejects(inf.run("e", async () => { throw new Error("boom"); })); assert.strictEqual(inf.size, 0); }); test("cache: TTL, null explicite, expiration", async () => { const c = createCache({ ttlMs: 40, max: 10 }); c.set("a", 1); c.set("n", null); assert.strictEqual(c.get("a"), 1); assert.strictEqual(c.get("n"), null); assert.strictEqual(c.get("absent"), undefined); c.set("z", 1, 0); // TTL 0 → pas mis en cache assert.strictEqual(c.get("z"), undefined); await sleep(60); assert.strictEqual(c.get("a"), undefined); assert.strictEqual(c.size, 1); // "n" expiré mais pas encore lu }); test("cache: taille max et éviction LRU", () => { const c = createCache({ ttlMs: 10000, max: 3 }); c.set("a", 1); c.set("b", 2); c.set("c", 3); c.get("a"); // a devient le plus récent c.set("d", 4); assert.strictEqual(c.size, 3); assert.strictEqual(c.get("b"), undefined); assert.strictEqual(c.get("a"), 1); assert.strictEqual(c.get("d"), 4); }); test("cache.wrap: hit, dédup en cours, null non mis en cache, ttlFor", async () => { const c = createCache({ ttlMs: 10000, max: 10 }); let calls = 0; const fn = async () => { calls++; await sleep(10); return { x: 1 }; }; const [a, b] = await Promise.all([c.wrap("k", fn), c.wrap("k", fn)]); assert.strictEqual(a, b); assert.strictEqual(calls, 1); await c.wrap("k", fn); assert.strictEqual(calls, 1); let nulls = 0; await c.wrap("n", async () => { nulls++; return null; }); await c.wrap("n", async () => { nulls++; return null; }); assert.strictEqual(nulls, 2); await c.wrap("m", async () => { nulls++; return null; }, { ttlFor: v => v === null ? 5000 : 10000 }); await c.wrap("m", async () => { nulls++; return null; }, { ttlFor: v => v === null ? 5000 : 10000 }); assert.strictEqual(nulls, 3); await assert.rejects(c.wrap("e", async () => { throw new Error("err"); })); assert.strictEqual(c.get("e"), undefined); const s = c.stats(); assert.ok(s.hits >= 2 && s.misses >= 4, JSON.stringify(s)); }); (async () => { let failed = 0; for (const t of tests) { try { await t.fn(); console.log("OK " + t.name); } catch (e) { failed++; console.log("FAIL " + t.name + "\n " + (e && e.stack || e)); } } console.log(`\n${tests.length - failed}/${tests.length} réussis`); process.exit(failed ? 1 : 0); })();