/**
* Tests for HttpCacheWrapper
*/
import { CacheManager } from "../CacheManager.js";
import { HttpCacheWrapper } from "../HttpCacheWrapper.js";
describe("HttpCacheWrapper", () => {
let cacheManager: CacheManager;
let httpCache: HttpCacheWrapper;
let mockRequestFn: jest.Mock;
beforeEach(() => {
cacheManager = new CacheManager({
maxSize: 100,
defaultTTL: 1000,
enableLRU: true,
enableStats: true,
});
httpCache = new HttpCacheWrapper(cacheManager, "test-site");
mockRequestFn = jest.fn();
});
afterEach(() => {
cacheManager.clear();
jest.clearAllMocks();
});
describe("Request Caching", () => {
test("should cache GET requests", async () => {
const mockResponse = {
data: { id: 1, title: "Test Post" },
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
const requestOptions = {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts",
headers: {},
params: {},
};
// First request should hit the API
const result1 = await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(1);
expect(result1.data).toEqual(mockResponse.data);
expect(result1.cached).toBe(false);
// Second request should use cache
const result2 = await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(1); // No additional call
expect(result2.data).toEqual(mockResponse.data);
expect(result2.cached).toBe(true);
});
test("should not cache non-GET requests", async () => {
const mockResponse = {
data: { id: 1, title: "Test Post" },
status: 201,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
const requestOptions = {
method: "POST",
url: "https://example.com/wp-json/wp/v2/posts",
headers: {},
params: {},
data: { title: "New Post" },
};
// Both requests should hit the API
const result1 = await httpCache.request(mockRequestFn, requestOptions);
const result2 = await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(2);
expect(result1.cached).toBeUndefined();
expect(result2.cached).toBeUndefined();
});
test("should not cache error responses", async () => {
const errorResponse = {
data: { error: "Not found" },
status: 404,
headers: {},
};
mockRequestFn.mockResolvedValue(errorResponse);
const requestOptions = {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts/999",
headers: {},
params: {},
};
// Both requests should hit the API (no caching of 404)
await httpCache.request(mockRequestFn, requestOptions);
await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(2);
});
test("should generate ETags for responses", async () => {
const mockResponse = {
data: { id: 1, title: "Test Post" },
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
const requestOptions = {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts/1",
headers: {},
params: {},
};
const result = await httpCache.request(mockRequestFn, requestOptions);
expect(result.headers.etag).toBeDefined();
expect(result.headers.etag).toMatch(/^"[a-f0-9]{32}"$/);
expect(result.headers["last-modified"]).toBeDefined();
expect(result.headers["cache-control"]).toBeDefined();
});
});
describe("Cache Invalidation", () => {
test("should invalidate specific endpoint", async () => {
// Cache a response
const mockResponse = {
data: { id: 1, title: "Test Post" },
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
const requestOptions = {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts/1",
headers: {},
params: {},
};
await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(1);
// Verify cache hit
await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(1);
// Invalidate cache
httpCache.invalidate("posts/1");
// Should hit API again
await httpCache.request(mockRequestFn, requestOptions);
expect(mockRequestFn).toHaveBeenCalledTimes(2);
});
test("should invalidate by pattern", async () => {
// Cache multiple responses
const responses = [
{ url: "posts", data: [{ id: 1 }, { id: 2 }] },
{ url: "posts/1", data: { id: 1 } },
{ url: "pages/1", data: { id: 1 } },
];
for (const response of responses) {
mockRequestFn.mockResolvedValueOnce({
data: response.data,
status: 200,
headers: {},
});
await httpCache.request(mockRequestFn, {
method: "GET",
url: `https://example.com/wp-json/wp/v2/${response.url}`,
headers: {},
params: {},
});
}
expect(mockRequestFn).toHaveBeenCalledTimes(3);
// Invalidate all posts
const invalidated = httpCache.invalidatePattern("posts");
expect(invalidated).toBe(2); // Should invalidate 2 post-related entries
// Verify pages cache still works
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/pages/1",
headers: {},
params: {},
});
expect(mockRequestFn).toHaveBeenCalledTimes(3); // No new call for pages
});
test("should invalidate all cache for site", async () => {
// Cache some responses
const mockResponse = {
data: { id: 1 },
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts",
headers: {},
params: {},
});
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/pages",
headers: {},
params: {},
});
expect(mockRequestFn).toHaveBeenCalledTimes(2);
// Clear all cache
const invalidated = httpCache.invalidateAll();
expect(invalidated).toBeGreaterThan(0);
// Verify cache is cleared
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts",
headers: {},
params: {},
});
expect(mockRequestFn).toHaveBeenCalledTimes(3);
});
});
describe("Cache Warming", () => {
test("should pre-warm cache with data", () => {
const endpoint = "posts";
const data = [
{ id: 1, title: "Post 1" },
{ id: 2, title: "Post 2" },
];
const params = { per_page: 10 };
httpCache.warm(endpoint, data, params);
// Verify cache entry exists
const cacheKey = cacheManager.generateKey("test-site", endpoint, params);
const cached = cacheManager.get(cacheKey);
expect(cached).toBeDefined();
// Test cache structure without conditional expects
const cachedEntry = cached as { data: unknown; etag?: string; lastModified?: string };
expect(cachedEntry).toHaveProperty("data");
expect(cachedEntry).toHaveProperty("etag");
expect(cachedEntry).toHaveProperty("lastModified");
expect(cachedEntry.data).toEqual(data);
});
test("should use warmed cache for requests", async () => {
const endpoint = "posts";
const data = [{ id: 1, title: "Post 1" }];
httpCache.warm(endpoint, data);
const result = await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts",
headers: {},
params: {},
});
expect(mockRequestFn).not.toHaveBeenCalled();
expect(result.data).toEqual(data);
expect(result.cached).toBe(true);
});
});
describe("Cache Statistics", () => {
test("should return cache statistics", async () => {
const mockResponse = {
data: { id: 1 },
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
// Generate some cache activity
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts/1",
headers: {},
params: {},
});
await httpCache.request(mockRequestFn, {
method: "GET",
url: "https://example.com/wp-json/wp/v2/posts/1",
headers: {},
params: {},
});
const stats = httpCache.getStats();
expect(stats.hits).toBe(1);
expect(stats.misses).toBe(1);
expect(stats.totalSize).toBe(1);
expect(stats.hitRate).toBe(0.5);
});
});
describe("URL and Endpoint Extraction", () => {
test("should extract endpoint from WordPress API URLs", async () => {
const testCases = [
{
url: "https://example.com/wp-json/wp/v2/posts",
expectedEndpoint: "posts",
},
{
url: "https://example.com/wp-json/wp/v2/posts/123",
expectedEndpoint: "posts/123",
},
{
url: "https://example.com/wp-json/wp/v2/categories?per_page=10",
expectedEndpoint: "categories",
},
];
const mockResponse = {
data: {},
status: 200,
headers: {},
};
mockRequestFn.mockResolvedValue(mockResponse);
for (const testCase of testCases) {
await httpCache.request(mockRequestFn, {
method: "GET",
url: testCase.url,
headers: {},
params: {},
});
// Verify cache key contains the correct endpoint
const stats = httpCache.getStats();
expect(stats.totalSize).toBeGreaterThan(0);
}
});
});
});