add unit tests for usage middleware (14 tests)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m53s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m53s
This commit is contained in:
parent
1aea9c872c
commit
c01e88686a
1 changed files with 231 additions and 0 deletions
231
src/__tests__/usage.test.ts
Normal file
231
src/__tests__/usage.test.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Unmock usage middleware — we want to test the real implementation
|
||||
vi.unmock("../middleware/usage.js");
|
||||
|
||||
// DB and keys are still mocked by setup.ts
|
||||
import { queryWithRetry } from "../services/db.js";
|
||||
import { isProKey } from "../services/keys.js";
|
||||
|
||||
describe("usage middleware", () => {
|
||||
let usage: typeof import("../middleware/usage.js");
|
||||
|
||||
const mockJson = vi.fn();
|
||||
const mockStatus = vi.fn(() => ({ json: mockJson }));
|
||||
const mockNext = vi.fn();
|
||||
|
||||
function makeReq(key = "df_free_testkey123"): any {
|
||||
return { apiKeyInfo: { key } };
|
||||
}
|
||||
|
||||
function makeRes(): any {
|
||||
mockJson.mockClear();
|
||||
mockStatus.mockClear();
|
||||
return { status: mockStatus, json: mockJson };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
// Re-import to get fresh internal state (new Map)
|
||||
usage = await import("../middleware/usage.js");
|
||||
});
|
||||
|
||||
// --- loadUsageData ---
|
||||
|
||||
describe("loadUsageData", () => {
|
||||
it("populates in-memory map from DB rows", async () => {
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [
|
||||
{ key: "df_free_abc12345", count: 42, month_key: "2026-02" },
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
await usage.loadUsageData();
|
||||
|
||||
const stats = usage.getUsageStats("df_free_abc12345");
|
||||
expect(stats["df_free_..."]).toEqual({ count: 42, month: "2026-02" });
|
||||
});
|
||||
|
||||
it("handles empty DB result gracefully", async () => {
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
await usage.loadUsageData();
|
||||
const stats = usage.getUsageStats("df_free_nonexist");
|
||||
expect(stats).toEqual({});
|
||||
});
|
||||
|
||||
it("handles DB error gracefully (starts fresh)", async () => {
|
||||
vi.mocked(queryWithRetry).mockRejectedValueOnce(new Error("DB down"));
|
||||
await usage.loadUsageData();
|
||||
const stats = usage.getUsageStats("anything");
|
||||
expect(stats).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// --- getUsageStats ---
|
||||
|
||||
describe("getUsageStats", () => {
|
||||
it("returns empty object when key not found", () => {
|
||||
expect(usage.getUsageStats("nonexistent")).toEqual({});
|
||||
});
|
||||
|
||||
it("returns empty object when no key provided", () => {
|
||||
expect(usage.getUsageStats()).toEqual({});
|
||||
});
|
||||
|
||||
it("masks key to first 8 chars + '...'", async () => {
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key: "df_free_longkeyvalue", count: 5, month_key: "2026-02" }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await usage.loadUsageData();
|
||||
|
||||
const stats = usage.getUsageStats("df_free_longkeyvalue");
|
||||
const keys = Object.keys(stats);
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0]).toBe("df_free_...");
|
||||
});
|
||||
});
|
||||
|
||||
// --- usageMiddleware ---
|
||||
|
||||
describe("usageMiddleware", () => {
|
||||
it("calls next() for free key under limit", () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const req = makeReq();
|
||||
const res = makeRes();
|
||||
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls next() for pro key under limit", () => {
|
||||
vi.mocked(isProKey).mockReturnValue(true);
|
||||
const req = makeReq("df_pro_testkey123");
|
||||
const res = makeRes();
|
||||
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 429 when free key exceeds 100/month", async () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const key = "df_free_limited1234";
|
||||
|
||||
// Seed usage at the limit
|
||||
const monthKey = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key, count: 100, month_key: monthKey }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await usage.loadUsageData();
|
||||
|
||||
const req = makeReq(key);
|
||||
const res = makeRes();
|
||||
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockStatus).toHaveBeenCalledWith(429);
|
||||
expect(mockJson).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: expect.stringContaining("Free tier limit") })
|
||||
);
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 429 when pro key exceeds 5000/month", async () => {
|
||||
vi.mocked(isProKey).mockReturnValue(true);
|
||||
const key = "df_pro_limited12345";
|
||||
|
||||
const monthKey = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key, count: 5000, month_key: monthKey }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await usage.loadUsageData();
|
||||
|
||||
const req = makeReq(key);
|
||||
const res = makeRes();
|
||||
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockStatus).toHaveBeenCalledWith(429);
|
||||
expect(mockJson).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: expect.stringContaining("Pro tier limit") })
|
||||
);
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("increments usage count on each call", () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const key = "df_free_counting123";
|
||||
|
||||
// Call middleware 3 times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
usage.usageMiddleware(makeReq(key), makeRes(), mockNext);
|
||||
}
|
||||
|
||||
const stats = usage.getUsageStats(key);
|
||||
const masked = Object.keys(stats)[0];
|
||||
expect(stats[masked].count).toBe(3);
|
||||
});
|
||||
|
||||
it("resets count when month changes", async () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const key = "df_free_monthreset";
|
||||
|
||||
// Seed with old month data at limit
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key, count: 100, month_key: "2025-01" }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await usage.loadUsageData();
|
||||
|
||||
// Current month is different, so it should reset and allow
|
||||
const req = makeReq(key);
|
||||
const res = makeRes();
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockStatus).not.toHaveBeenCalled();
|
||||
|
||||
// Count should be 1 (reset + this request)
|
||||
const stats = usage.getUsageStats(key);
|
||||
const masked = Object.keys(stats)[0];
|
||||
expect(stats[masked].count).toBe(1);
|
||||
});
|
||||
|
||||
it("allows free key at count 99 (just under limit)", async () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const key = "df_free_almost1234";
|
||||
const monthKey = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key, count: 99, month_key: monthKey }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await usage.loadUsageData();
|
||||
|
||||
const req = makeReq(key);
|
||||
const res = makeRes();
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles missing apiKeyInfo gracefully (uses 'unknown')", () => {
|
||||
vi.mocked(isProKey).mockReturnValue(false);
|
||||
const req = { apiKeyInfo: undefined } as any;
|
||||
const res = makeRes();
|
||||
|
||||
usage.usageMiddleware(req, res, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue