feat(core-testing): add RecordingRateLimit for test assertions
Captures consume + reset call arguments verbatim via consumeCalls and resetCalls accessors. Uses local IRateLimit type alias (no core-shared dep) following the recording-job-queue pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export * from "./factory/index.js";
|
export * from "./factory/index.js";
|
||||||
export * from "./contract/index.js";
|
export * from "./contract/index.js";
|
||||||
export * from "./instrumentation/index.js";
|
export * from "./instrumentation/index.js";
|
||||||
|
export * from "./rate-limit/index.js";
|
||||||
|
|||||||
5
packages/core-testing/src/rate-limit/index.ts
Normal file
5
packages/core-testing/src/rate-limit/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
RecordingRateLimit,
|
||||||
|
type RecordedConsumeCall,
|
||||||
|
type RecordedResetCall,
|
||||||
|
} from "./recording-rate-limit";
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { RecordingRateLimit } from "@/rate-limit/recording-rate-limit";
|
||||||
|
|
||||||
|
describe("RecordingRateLimit", () => {
|
||||||
|
it("starts with empty call arrays", () => {
|
||||||
|
const rl = new RecordingRateLimit();
|
||||||
|
expect(rl.consumeCalls).toHaveLength(0);
|
||||||
|
expect(rl.resetCalls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures consume call arguments verbatim including optional weight", async () => {
|
||||||
|
const rl = new RecordingRateLimit();
|
||||||
|
await rl.consume("api", "user:123");
|
||||||
|
await rl.consume("api", "user:456", 2);
|
||||||
|
expect(rl.consumeCalls).toEqual([
|
||||||
|
{ budgetName: "api", key: "user:123", weight: undefined },
|
||||||
|
{ budgetName: "api", key: "user:456", weight: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures reset call arguments verbatim", async () => {
|
||||||
|
const rl = new RecordingRateLimit();
|
||||||
|
await rl.reset("api", "user:123");
|
||||||
|
await rl.reset("auth", "ip:1.2.3.4");
|
||||||
|
expect(rl.resetCalls).toEqual([
|
||||||
|
{ budgetName: "api", key: "user:123" },
|
||||||
|
{ budgetName: "auth", key: "ip:1.2.3.4" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns allowed decision by default", async () => {
|
||||||
|
const rl = new RecordingRateLimit();
|
||||||
|
const decision = await rl.consume("api", "user:123");
|
||||||
|
expect(decision).toEqual({
|
||||||
|
allowed: true,
|
||||||
|
remaining: Infinity,
|
||||||
|
resetAt: new Date(0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns configured decision after withDecision", async () => {
|
||||||
|
const rl = new RecordingRateLimit();
|
||||||
|
const blocked = { allowed: false, remaining: 0, resetAt: new Date(9999) };
|
||||||
|
rl.withDecision(blocked);
|
||||||
|
const decision = await rl.consume("api", "user:123");
|
||||||
|
expect(decision).toEqual(blocked);
|
||||||
|
});
|
||||||
|
});
|
||||||
60
packages/core-testing/src/rate-limit/recording-rate-limit.ts
Normal file
60
packages/core-testing/src/rate-limit/recording-rate-limit.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Local type aliases matching the contracts in @repo/core-shared/rate-limit.
|
||||||
|
// Kept inline to avoid a build-graph cycle between core-testing and core-shared
|
||||||
|
// (mirrors the recording-job-queue pattern).
|
||||||
|
|
||||||
|
type RateLimitDecision = {
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
resetAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IRateLimit {
|
||||||
|
consume(
|
||||||
|
budgetName: string,
|
||||||
|
key: string,
|
||||||
|
weight?: number,
|
||||||
|
): Promise<RateLimitDecision>;
|
||||||
|
reset(budgetName: string, key: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecordedConsumeCall = {
|
||||||
|
budgetName: string;
|
||||||
|
key: string;
|
||||||
|
weight: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecordedResetCall = {
|
||||||
|
budgetName: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EPOCH = new Date(0);
|
||||||
|
|
||||||
|
export class RecordingRateLimit implements IRateLimit {
|
||||||
|
readonly consumeCalls: RecordedConsumeCall[] = [];
|
||||||
|
readonly resetCalls: RecordedResetCall[] = [];
|
||||||
|
|
||||||
|
private _decision: RateLimitDecision = {
|
||||||
|
allowed: true,
|
||||||
|
remaining: Infinity,
|
||||||
|
resetAt: EPOCH,
|
||||||
|
};
|
||||||
|
|
||||||
|
withDecision(decision: RateLimitDecision): this {
|
||||||
|
this._decision = decision;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async consume(
|
||||||
|
budgetName: string,
|
||||||
|
key: string,
|
||||||
|
weight?: number,
|
||||||
|
): Promise<RateLimitDecision> {
|
||||||
|
this.consumeCalls.push({ budgetName, key, weight });
|
||||||
|
return this._decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset(budgetName: string, key: string): Promise<void> {
|
||||||
|
this.resetCalls.push({ budgetName, key });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user