addition of new dashboard
This commit is contained in:
96
backend/modules/core/apiKey.cache.test.js
Normal file
96
backend/modules/core/apiKey.cache.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { jest } from "@jest/globals";
|
||||
|
||||
// The bound is read per call from config, so set it before the first verify
|
||||
// rather than before the import. Own file so the tiny bound cannot leak into
|
||||
// apiKey.test.js.
|
||||
process.env.DASHBOARD_KEY_CACHE_MAX = "2";
|
||||
|
||||
const query = jest.fn();
|
||||
const release = jest.fn(async () => {});
|
||||
jest.unstable_mockModule("../../db/index.js", () => ({
|
||||
getPool: async () => ({ query, release }),
|
||||
}));
|
||||
|
||||
const { loadConfig } = await import("../../common/config.js");
|
||||
await loadConfig();
|
||||
|
||||
const { verifyKey, generateKey, _resetKeyCache } = await import("./apiKey.js");
|
||||
|
||||
beforeEach(() => {
|
||||
query.mockReset();
|
||||
release.mockClear();
|
||||
_resetKeyCache();
|
||||
});
|
||||
|
||||
async function mintKey(id) {
|
||||
query.mockReset();
|
||||
query
|
||||
.mockResolvedValueOnce({ rows: [{ id: 2 }] }) // SELECT app
|
||||
.mockResolvedValueOnce({ rows: [{ id, created_ts: "2026-01-01" }] }); // INSERT
|
||||
const out = await generateKey(1, "core", { label: `k${id}` });
|
||||
return { raw: out.key, keyHash: query.mock.calls[1][1][3], id };
|
||||
}
|
||||
|
||||
async function verifyWithDb({ raw, keyHash, id }) {
|
||||
query.mockReset();
|
||||
query
|
||||
.mockResolvedValueOnce({ rows: [{ id, user_id: 1, app_id: 2, key_hash: keyHash }] })
|
||||
.mockResolvedValue({ rows: [] });
|
||||
const result = await verifyKey(raw);
|
||||
return { result, selects: query.mock.calls.filter((c) => /SELECT/.test(c[0])) };
|
||||
}
|
||||
|
||||
async function servedFromCache({ raw }) {
|
||||
query.mockReset();
|
||||
query.mockResolvedValue({ rows: [] }); // if it does hit the DB, it finds nothing
|
||||
const result = await verifyKey(raw);
|
||||
return result !== null && query.mock.calls.length === 0;
|
||||
}
|
||||
|
||||
describe("validated-key cache bound", () => {
|
||||
it("evicts the least recently used key once the cache is full", async () => {
|
||||
const a = await mintKey(11);
|
||||
const b = await mintKey(12);
|
||||
const c = await mintKey(13);
|
||||
|
||||
// Fill the cache to its bound of 2.
|
||||
await verifyWithDb(a);
|
||||
await verifyWithDb(b);
|
||||
|
||||
// A hit counts as a use, so probes below check the EVICTED key first.
|
||||
expect(await servedFromCache(a)).toBe(true);
|
||||
|
||||
await verifyWithDb(c);
|
||||
expect(await servedFromCache(b)).toBe(false);
|
||||
expect(await servedFromCache(a)).toBe(true);
|
||||
expect(await servedFromCache(c)).toBe(true);
|
||||
|
||||
const { result, selects } = await verifyWithDb(b);
|
||||
expect(result).toEqual({ user_id: 1, app_id: 2 });
|
||||
expect(selects).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("never grows past the bound, however many distinct keys verify", async () => {
|
||||
const keys = [];
|
||||
for (let i = 0; i < 6; i++) keys.push(await mintKey(20 + i));
|
||||
for (const k of keys) await verifyWithDb(k);
|
||||
|
||||
const cached = [];
|
||||
for (const k of keys) if (await servedFromCache(k)) cached.push(k.id);
|
||||
expect(cached).toEqual([keys[4].id, keys[5].id]);
|
||||
});
|
||||
|
||||
it("a cache hit refreshes recency without extending the entry's TTL", async () => {
|
||||
const a = await mintKey(31);
|
||||
await verifyWithDb(a);
|
||||
|
||||
const realNow = Date.now;
|
||||
try {
|
||||
// Must expire on its original deadline despite being touched.
|
||||
Date.now = () => realNow() + 31 * 60 * 1000;
|
||||
expect(await servedFromCache(a)).toBe(false);
|
||||
} finally {
|
||||
Date.now = realNow;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user