Spaces:
Sleeping
Sleeping
| 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); | |
| }); | |
| }); | |