Skip to main content
Glama
CacheManager.test.js12.8 kB
/** * Tests for CacheManager * * Tests the in-memory caching system with TTL and LRU eviction. * Matches the actual CacheManager implementation. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { CacheManager } from "@/cache/CacheManager.js"; // Mock config with proper structure that matches Config.js exports vi.mock("../../dist/config/Config.js", () => ({ config: () => ({ app: { isDevelopment: true, isProduction: false, isTest: true, isDXT: false, isCI: false, }, }), ConfigHelpers: { isTest: vi.fn(() => true), }, })); describe("CacheManager", () => { let cacheManager; let mockConfig; beforeEach(() => { mockConfig = { maxSize: 100, defaultTTL: 300000, // 5 minutes in milliseconds enableLRU: true, enableStats: true, }; cacheManager = new CacheManager(mockConfig); vi.clearAllMocks(); }); afterEach(() => { if (cacheManager && cacheManager.destroy) { cacheManager.destroy(); } vi.restoreAllMocks(); }); describe("Constructor", () => { it("should initialize with configuration", () => { expect(cacheManager).toBeDefined(); expect(cacheManager.config).toEqual(mockConfig); }); it("should initialize with default stats", () => { const stats = cacheManager.getStats(); expect(stats.hits).toBe(0); expect(stats.misses).toBe(0); expect(stats.evictions).toBe(0); expect(stats.totalSize).toBe(0); expect(stats.hitRate).toBe(0); }); }); describe("Basic Cache Operations", () => { it("should set and get values", () => { cacheManager.set("test-key", "test-value"); const value = cacheManager.get("test-key"); expect(value).toBe("test-value"); }); it("should return null for non-existent keys", () => { const value = cacheManager.get("non-existent-key"); expect(value).toBeNull(); }); it("should check if keys exist", () => { cacheManager.set("exists-key", "exists-value"); const exists = cacheManager.has("exists-key"); const notExists = cacheManager.has("not-exists-key"); expect(exists).toBe(true); expect(notExists).toBe(false); }); it("should delete values", () => { cacheManager.set("delete-key", "delete-value"); let value = cacheManager.get("delete-key"); expect(value).toBe("delete-value"); const deleted = cacheManager.delete("delete-key"); expect(deleted).toBe(true); value = cacheManager.get("delete-key"); expect(value).toBeNull(); }); it("should clear all cache entries", () => { cacheManager.set("key1", "value1"); cacheManager.set("key2", "value2"); cacheManager.set("key3", "value3"); cacheManager.clear(); const value1 = cacheManager.get("key1"); const value2 = cacheManager.get("key2"); const value3 = cacheManager.get("key3"); expect(value1).toBeNull(); expect(value2).toBeNull(); expect(value3).toBeNull(); }); }); describe("Key Generation", () => { it("should generate keys with site and endpoint", () => { const key = cacheManager.generateKey("site1", "posts"); expect(key).toBe("site1:posts"); }); it("should generate keys with parameters hash", () => { const key = cacheManager.generateKey("site1", "posts", { status: "publish", per_page: 10 }); // fastHash uses base36 encoding (0-9, a-z) for compact keys expect(key).toMatch(/^site1:posts:[a-z0-9]+$/); }); it("should generate consistent keys for same parameters", () => { const params = { status: "publish", per_page: 10 }; const key1 = cacheManager.generateKey("site1", "posts", params); const key2 = cacheManager.generateKey("site1", "posts", params); expect(key1).toBe(key2); }); it("should generate different keys for different parameter order (normalized)", () => { const params1 = { status: "publish", per_page: 10 }; const params2 = { per_page: 10, status: "publish" }; const key1 = cacheManager.generateKey("site1", "posts", params1); const key2 = cacheManager.generateKey("site1", "posts", params2); // Should be the same because parameters are normalized (sorted) expect(key1).toBe(key2); }); }); describe("TTL and Expiration", () => { it("should handle TTL expiration", (done) => { // Use very short TTL for testing cacheManager.set("ttl-key", "ttl-value", 50); // 50ms TTL // Should exist immediately let value = cacheManager.get("ttl-key"); expect(value).toBe("ttl-value"); // Wait for expiration setTimeout(() => { value = cacheManager.get("ttl-key"); expect(value).toBeNull(); done(); }, 100); }); it("should use custom TTL", () => { cacheManager.set("custom-ttl", "value", 60000); // 1 minute // Check the entry directly const entry = cacheManager.getEntry("custom-ttl"); expect(entry).toBeDefined(); expect(entry.ttl).toBe(60000); }); it("should use default TTL when not specified", () => { cacheManager.set("default-ttl", "value"); const entry = cacheManager.getEntry("default-ttl"); expect(entry).toBeDefined(); expect(entry.ttl).toBe(mockConfig.defaultTTL); }); }); describe("Cache Statistics", () => { it("should track cache hits and misses", () => { cacheManager.set("stats-key", "stats-value"); // Hit cacheManager.get("stats-key"); // Miss cacheManager.get("non-existent-key"); const stats = cacheManager.getStats(); expect(stats.hits).toBe(1); expect(stats.misses).toBe(1); expect(stats.hitRate).toBe(0.5); }); it("should track total size", () => { cacheManager.set("key1", "value1"); cacheManager.set("key2", "value2"); const stats = cacheManager.getStats(); expect(stats.totalSize).toBe(2); }); it("should track evictions", () => { // Set cache to small size to trigger evictions const smallConfig = { ...mockConfig, maxSize: 2 }; const smallCache = new CacheManager(smallConfig); smallCache.set("key1", "value1"); smallCache.set("key2", "value2"); smallCache.set("key3", "value3"); // Should trigger eviction const stats = smallCache.getStats(); expect(stats.evictions).toBe(1); expect(stats.totalSize).toBe(2); smallCache.destroy(); }); }); describe("LRU Eviction", () => { it("should evict least recently used entries when maxSize exceeded", () => { const smallConfig = { ...mockConfig, maxSize: 3 }; const lruCache = new CacheManager(smallConfig); lruCache.set("oldest", "value1"); lruCache.set("middle", "value2"); lruCache.set("newest", "value3"); // Access middle to make it more recent lruCache.get("middle"); // Add new item to trigger eviction of oldest lruCache.set("newer", "value4"); const oldest = lruCache.get("oldest"); const middle = lruCache.get("middle"); const newest = lruCache.get("newest"); const newer = lruCache.get("newer"); expect(oldest).toBeNull(); // Should be evicted (least recently used) expect(middle).toBe("value2"); // Should remain (accessed recently) expect(newest).toBe("value3"); expect(newer).toBe("value4"); lruCache.destroy(); }); }); describe("Site-specific Operations", () => { it("should clear cache entries for specific site", () => { cacheManager.set("site1:posts", "site1-posts"); cacheManager.set("site1:pages", "site1-pages"); cacheManager.set("site2:posts", "site2-posts"); const cleared = cacheManager.clearSite("site1"); expect(cleared).toBe(2); expect(cacheManager.get("site1:posts")).toBeNull(); expect(cacheManager.get("site1:pages")).toBeNull(); expect(cacheManager.get("site2:posts")).toBe("site2-posts"); }); it("should clear cache entries matching pattern", () => { cacheManager.set("posts:list", "posts"); cacheManager.set("posts:single:123", "post123"); cacheManager.set("pages:list", "pages"); const cleared = cacheManager.clearPattern(/^posts:/); expect(cleared).toBe(2); expect(cacheManager.get("posts:list")).toBeNull(); expect(cacheManager.get("posts:single:123")).toBeNull(); expect(cacheManager.get("pages:list")).toBe("pages"); }); }); describe("Conditional Requests", () => { it("should support etag for conditional requests", () => { cacheManager.set("etag-key", "value", 300000, "etag-123"); const supportsConditional = cacheManager.supportsConditionalRequest("etag-key"); expect(supportsConditional).toBe(true); const headers = cacheManager.getConditionalHeaders("etag-key"); expect(headers["If-None-Match"]).toBe("etag-123"); }); it("should support lastModified for conditional requests", () => { const lastModified = "Wed, 21 Oct 2015 07:28:00 GMT"; cacheManager.set("lastmod-key", "value", 300000, undefined, lastModified); const supportsConditional = cacheManager.supportsConditionalRequest("lastmod-key"); expect(supportsConditional).toBe(true); const headers = cacheManager.getConditionalHeaders("lastmod-key"); expect(headers["If-Modified-Since"]).toBe(lastModified); }); it("should support both etag and lastModified", () => { const etag = "etag-456"; const lastModified = "Wed, 21 Oct 2015 07:28:00 GMT"; cacheManager.set("both-key", "value", 300000, etag, lastModified); const headers = cacheManager.getConditionalHeaders("both-key"); expect(headers["If-None-Match"]).toBe(etag); expect(headers["If-Modified-Since"]).toBe(lastModified); }); }); describe("Data Types", () => { it("should handle string values", () => { cacheManager.set("string-key", "string-value"); const value = cacheManager.get("string-key"); expect(value).toBe("string-value"); expect(typeof value).toBe("string"); }); it("should handle number values", () => { cacheManager.set("number-key", 42); const value = cacheManager.get("number-key"); expect(value).toBe(42); expect(typeof value).toBe("number"); }); it("should handle boolean values", () => { cacheManager.set("boolean-key", true); const value = cacheManager.get("boolean-key"); expect(value).toBe(true); expect(typeof value).toBe("boolean"); }); it("should handle object values", () => { const testObject = { id: 1, name: "Test", active: true }; cacheManager.set("object-key", testObject); const value = cacheManager.get("object-key"); expect(value).toEqual(testObject); expect(typeof value).toBe("object"); }); it("should handle array values", () => { const testArray = [1, 2, 3, "four", { five: 5 }]; cacheManager.set("array-key", testArray); const value = cacheManager.get("array-key"); expect(value).toEqual(testArray); expect(Array.isArray(value)).toBe(true); }); it("should handle null values", () => { cacheManager.set("null-key", null); const value = cacheManager.get("null-key"); expect(value).toBeNull(); }); }); describe("Cache Entry Metadata", () => { it("should track access count and last accessed time", async () => { cacheManager.set("access-key", "value"); // First access cacheManager.get("access-key"); let entry = cacheManager.getEntry("access-key"); expect(entry.accessCount).toBe(2); // 1 from set + 1 from get // Wait a tiny bit to ensure different timestamps await new Promise((resolve) => setTimeout(resolve, 1)); // Second access cacheManager.get("access-key"); entry = cacheManager.getEntry("access-key"); expect(entry.accessCount).toBe(3); // 1 from set + 2 from gets expect(entry.lastAccessed).toBeGreaterThanOrEqual(entry.timestamp); }); }); describe("Cache Presets", () => { it("should export cache presets", async () => { const { CachePresets } = await import("@/cache/CacheManager.js"); expect(CachePresets).toBeDefined(); expect(CachePresets.STATIC).toBeDefined(); expect(CachePresets.DYNAMIC).toBeDefined(); expect(CachePresets.SESSION).toBeDefined(); expect(CachePresets.REALTIME).toBeDefined(); expect(CachePresets.STATIC.ttl).toBe(4 * 60 * 60 * 1000); // 4 hours expect(CachePresets.DYNAMIC.ttl).toBe(15 * 60 * 1000); // 15 minutes }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server