import { describe, expect, test } from "bun:test"; import { cachedPermissionStore, memoryPermissionStore } from "../src/store.ts"; import { runStoreConformance } from "./store-conformance.ts"; // A cache must not change observable behaviour: writes invalidate internally. runStoreConformance("cached(memory)", async () => cachedPermissionStore(memoryPermissionStore())); describe("cachedPermissionStore", () => { test("serves a repeat read from cache", async () => { const inner = memoryPermissionStore(); let reads = 0; const counting = { ...inner, assignmentsFor: (id: string, scope?: { tenantId?: string }) => { reads++; return inner.assignmentsFor(id, scope); }, }; const store = cachedPermissionStore(counting, { ttlMs: 60_000 }); await store.assignmentsFor("u1"); await store.assignmentsFor("u1"); expect(reads).toBe(1); }); test("a write invalidates that subject", async () => { const store = cachedPermissionStore(memoryPermissionStore(), { ttlMs: 60_000 }); await store.assignmentsFor("u1"); await store.assignRole("u1", "editor"); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("invalidate() drops a cached subject", async () => { const inner = memoryPermissionStore(); const store = cachedPermissionStore(inner, { ttlMs: 60_000 }); await store.assignmentsFor("u1"); await inner.assignRole("u1", "editor"); // behind the cache's back expect((await store.assignmentsFor("u1")).roles).toEqual([]); store.invalidate("u1"); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("entries expire after ttlMs", async () => { const inner = memoryPermissionStore(); const store = cachedPermissionStore(inner, { ttlMs: 1 }); await store.assignmentsFor("u1"); await inner.assignRole("u1", "editor"); await Bun.sleep(5); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("cache is bounded by max", async () => { const store = cachedPermissionStore(memoryPermissionStore(), { ttlMs: 60_000, max: 2 }); await store.assignmentsFor("a"); await store.assignmentsFor("b"); await store.assignmentsFor("c"); expect(store.size()).toBeLessThanOrEqual(2); }); test("scoped and global reads cache separately", async () => { const inner = memoryPermissionStore(); const store = cachedPermissionStore(inner, { ttlMs: 60_000 }); await inner.assignRole("u1", "editor", { tenantId: "t1" }); expect((await store.assignmentsFor("u1")).roles).toEqual([]); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]); }); test("a global write invalidates the subject in every tenant", async () => { const inner = memoryPermissionStore(); const store = cachedPermissionStore(inner, { ttlMs: 60_000 }); await store.assignmentsFor("u1", { tenantId: "t1" }); // warm the tenant entry await store.assignRole("u1", "editor"); // global write expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]); }); test("cache keys cannot collide across subject/tenant boundaries", async () => { const inner = memoryPermissionStore(); const store = cachedPermissionStore(inner, { ttlMs: 60_000 }); await inner.assignRole("b\uFFFDc", "editor", { tenantId: "a" }); expect((await store.assignmentsFor("b\uFFFDc", { tenantId: "a" })).roles).toEqual(["editor"]); expect((await store.assignmentsFor("c", { tenantId: "a\uFFFDb" })).roles).toEqual([]); }); });