better-chatbot / src /lib /cache /safe-redis-cache.test.ts
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
5.88 kB
import { describe, it, expect, beforeEach, vi } from "vitest";
import { SafeRedisCache } from "./safe-redis-cache";
import { MemoryCache } from "./memory-cache";
import { RedisCache } from "./redis-cache";
vi.mock("./redis-cache");
vi.mock("logger", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
describe("SafeRedisCache", () => {
let cache: SafeRedisCache;
let mockRedisCache: any;
let mockMemoryCache: any;
beforeEach(() => {
vi.clearAllMocks();
mockRedisCache = {
get: vi.fn(),
set: vi.fn(),
has: vi.fn(),
delete: vi.fn(),
clear: vi.fn(),
getAll: vi.fn(),
disconnect: vi.fn(),
};
mockMemoryCache = new MemoryCache();
});
it("should use Redis when available", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
mockRedisCache.get.mockResolvedValue("value");
const result = await cache.get("key");
expect(result).toBe("value");
expect(mockRedisCache.get).toHaveBeenCalledWith("key");
});
it("should fallback to memory cache when Redis fails", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
mockRedisCache.get.mockRejectedValue(new Error("Redis connection failed"));
await mockMemoryCache.set("key", "memoryValue");
const result = await cache.get("key");
expect(result).toBe("memoryValue");
});
it("should handle rate limit errors gracefully", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
mockRedisCache.set.mockRejectedValue(new Error("rate limit exceeded"));
await cache.set("key", "value");
const result = await mockMemoryCache.get("key");
expect(result).toBe("value");
});
it("should retry Redis connection after failure", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({
serverCache: mockMemoryCache,
retryDelay: 100, // Short delay for testing
});
// First call fails
mockRedisCache.get.mockRejectedValueOnce(new Error("Connection failed"));
await cache.get("key1");
// Wait for retry delay
await new Promise((resolve) => setTimeout(resolve, 150));
// Next call should attempt reconnection
mockRedisCache.has.mockResolvedValueOnce(true); // Connection test succeeds
mockRedisCache.get.mockResolvedValueOnce("value2");
const result = await cache.get("key2");
expect(mockRedisCache.has).toHaveBeenCalledWith("__test__");
expect(result).toBe("value2");
});
it("should set values in both caches when using Redis", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
mockRedisCache.set.mockResolvedValue(undefined);
await cache.set("key", "value", 1000);
expect(mockRedisCache.set).toHaveBeenCalledWith("key", "value", 1000);
expect(await mockMemoryCache.get("key")).toBe("value");
});
it("should delete from both caches", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
await mockMemoryCache.set("key", "value");
mockRedisCache.delete.mockResolvedValue(undefined);
await cache.delete("key");
expect(mockRedisCache.delete).toHaveBeenCalledWith("key");
expect(await mockMemoryCache.has("key")).toBe(false);
});
it("should clear both caches", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
await mockMemoryCache.set("key1", "value1");
await mockMemoryCache.set("key2", "value2");
mockRedisCache.clear.mockResolvedValue(undefined);
await cache.clear();
expect(mockRedisCache.clear).toHaveBeenCalled();
expect((await mockMemoryCache.getAll()).size).toBe(0);
});
it("should report cache status correctly", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
expect(cache.isUsingRedis()).toBe(true);
expect(cache.getCacheStatus()).toEqual({
redis: true,
retries: 0,
});
// Simulate Redis failure
mockRedisCache.get.mockRejectedValue(new Error("Connection failed"));
await cache.get("key");
expect(cache.isUsingRedis()).toBe(false);
expect(cache.getCacheStatus().redis).toBe(false);
});
it("should handle OOM errors", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({ serverCache: mockMemoryCache });
mockRedisCache.set.mockRejectedValue(new Error("OOM command not allowed"));
await cache.set("key", "value");
const result = await mockMemoryCache.get("key");
expect(result).toBe("value");
expect(cache.isUsingRedis()).toBe(false);
});
it("should respect max retries limit", async () => {
vi.mocked(RedisCache).mockImplementation(() => mockRedisCache);
cache = new SafeRedisCache({
serverCache: mockMemoryCache,
maxRetries: 2,
retryDelay: 50,
});
// Make Redis fail
mockRedisCache.get.mockRejectedValue(new Error("Connection failed"));
await cache.get("key");
// Attempt retries
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 60));
mockRedisCache.has.mockRejectedValue(new Error("Still failing"));
await cache.get(`key${i}`);
}
const status = cache.getCacheStatus();
expect(status.retries).toBeLessThanOrEqual(2);
});
});