Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
import { metrics } from "@opentelemetry/api";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { resetHibpMetricsCache } from "../observability/metrics.js";
import { RATE_LIMIT_MIN_GAP_MS, RateLimitGate } from "./rate-limit-gate.js";
interface CounterCall {
name: string;
value: number;
attrs: Record<string, string>;
}
function makeFakeMeterProvider(): {
counters: CounterCall[];
install(): void;
} {
const counters: CounterCall[] = [];
const provider = {
getMeter: () => ({
createCounter: (name: string) => ({
add: (v: number, attrs: Record<string, string> = {}) =>
counters.push({ name, value: v, attrs }),
}),
createHistogram: (name: string) => ({
record: (_v: number, _attrs?: Record<string, string>) => {
void name;
},
}),
}),
};
return {
counters,
install: () => {
metrics.disable();
metrics.setGlobalMeterProvider(
provider as unknown as Parameters<
typeof metrics.setGlobalMeterProvider
>[0]
);
resetHibpMetricsCache();
},
};
}
describe("RateLimitGate", () => {
it("exposes the documented 1500 ms minimum gap default", {
timeout: 5000,
}, () => {
expect(RATE_LIMIT_MIN_GAP_MS).toBe(1500);
});
it("second call within 1500ms window blocks until release", {
timeout: 5000,
}, async () => {
let now = 0;
const sleeps: number[] = [];
const gate = new RateLimitGate({
clock: () => now,
sleep: async (ms) => {
sleeps.push(ms);
now += ms;
},
});
await gate.acquire();
now = 500;
await gate.acquire();
expect(sleeps).toEqual([1000]);
expect(now).toBe(1500);
});
it("range calls bypass the gate (gate not invoked)", {
timeout: 5000,
}, () => {
// The gate enforces nothing for callers that do not call .acquire().
// This test documents that the range client path never constructs a
// gate — see range.ts which has no RateLimitGate import.
// (No-op assertion: structural proof captured in range.ts import set.)
expect(true).toBe(true);
});
it("throttle counter increments per blocked call", {
timeout: 5000,
}, async () => {
const fake = makeFakeMeterProvider();
fake.install();
let now = 0;
const gate = new RateLimitGate({
clock: () => now,
sleep: async (ms) => {
now += ms;
},
});
await gate.acquire();
now += 200;
await gate.acquire();
now += 200;
await gate.acquire();
const throttled = fake.counters.filter(
(c) => c.name === "hibp.rate_limit_throttled_total"
);
expect(throttled.length).toBe(2);
});
it("reset clears the next-allowed timestamp", {
timeout: 5000,
}, async () => {
let now = 0;
const sleeps: number[] = [];
const gate = new RateLimitGate({
clock: () => now,
sleep: async (ms) => {
sleeps.push(ms);
now += ms;
},
});
await gate.acquire();
gate.reset();
now = 100;
await gate.acquire();
expect(sleeps).toEqual([]);
});
it("returns early if signal aborted mid-wait", {
timeout: 5000,
}, async () => {
let now = 0;
const ac = new AbortController();
const gate = new RateLimitGate({
clock: () => now,
sleep: async (ms) => {
now += ms;
ac.abort();
},
});
await gate.acquire();
now = 100;
await gate.acquire(ac.signal);
// Gate returns; caller decides what to throw.
expect(ac.signal.aborted).toBe(true);
});
it("default sleep waits in wall clock when no override is provided", {
timeout: 5000,
}, async () => {
const gate = new RateLimitGate({ minGapMs: 0 });
await gate.acquire();
await gate.acquire();
expect(true).toBe(true);
});
});