/**
* 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
});
});
});