Initial Commit
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user