UNPKG

@safaricom-mxl/nextjs-turbo-redis-cache

Version:

Next.js redis cache handler

284 lines (219 loc) 8.55 kB
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DeduplicatedRequestHandler } from "../DeduplicatedRequestHandler"; describe("DeduplicatedRequestHandler", () => { let handler: DeduplicatedRequestHandler< (...args: [never, never]) => Promise<string>, string >; let mockFn: vi.MockedFunction<(...args: [never, never]) => Promise<string>>; let mockCache: any; beforeEach(() => { vi.clearAllMocks(); mockFn = vi.fn(); mockCache = { has: vi.fn(), get: vi.fn(), set: vi.fn(), delete: vi.fn(), }; handler = new DeduplicatedRequestHandler(mockFn, 1000, mockCache); }); afterEach(() => { vi.clearAllTimers(); vi.restoreAllMocks(); }); describe("seedRequestReturn", () => { it("should seed a result into the cache", () => { const key = "test-key"; const value = "test-value"; handler.seedRequestReturn(key, value); expect(mockCache.set).toHaveBeenCalledWith(key, expect.any(Promise)); // Verify the promise resolves to the correct value const promise = mockCache.set.mock.calls[0][1]; return expect(promise).resolves.toBe(value); }); it("should automatically remove the seeded result after caching time", async () => { vi.useFakeTimers(); const key = "test-key"; const value = "test-value"; handler.seedRequestReturn(key, value); expect(mockCache.delete).not.toHaveBeenCalled(); // Fast forward time vi.advanceTimersByTime(1000); expect(mockCache.delete).toHaveBeenCalledWith(key); vi.useRealTimers(); }); }); describe("deduplicatedFunction", () => { let deduplicatedFn: (...args: [never, never]) => Promise<string>; beforeEach(() => { deduplicatedFn = handler.deduplicatedFunction("test-key"); }); it("should call the original function when no cached result exists", async () => { mockCache.has.mockReturnValue(false); mockFn.mockResolvedValue("original-result"); const result = await deduplicatedFn( undefined as never, undefined as never ); expect(mockFn).toHaveBeenCalledWith(undefined, undefined); expect(mockCache.set).toHaveBeenCalledWith( "test-key", expect.any(Promise) ); expect(result).toBe("original-result"); }); it("should return cached result when available", async () => { const cachedPromise = Promise.resolve("cached-result"); mockCache.has.mockReturnValue(true); mockCache.get.mockReturnValue(cachedPromise); const result = await deduplicatedFn( undefined as never, undefined as never ); expect(mockFn).not.toHaveBeenCalled(); expect(result).toBe("cached-result"); }); it("should cache the promise from the original function", async () => { mockCache.has.mockReturnValue(false); mockFn.mockResolvedValue("function-result"); const promise = deduplicatedFn(undefined as never, undefined as never); expect(mockCache.set).toHaveBeenCalledWith("test-key", promise); expect(await promise).toBe("function-result"); }); it("should remove the cached result after caching timeout", async () => { vi.useFakeTimers(); mockCache.has.mockReturnValue(false); mockFn.mockResolvedValue("result"); await deduplicatedFn(undefined as never, undefined as never); expect(mockCache.delete).not.toHaveBeenCalled(); // Fast forward time beyond caching timeout vi.advanceTimersByTime(1000); expect(mockCache.delete).toHaveBeenCalledWith("test-key"); vi.useRealTimers(); }); it("should handle function errors and still clean up cache", async () => { vi.useFakeTimers(); mockCache.has.mockReturnValue(false); mockFn.mockRejectedValue(new Error("Function error")); await expect( deduplicatedFn(undefined as never, undefined as never) ).rejects.toThrow("Function error"); // Should still clean up after timeout vi.advanceTimersByTime(1000); expect(mockCache.delete).toHaveBeenCalledWith("test-key"); vi.useRealTimers(); }); it("should return structurally cloned results to prevent mutation", async () => { const originalObject = { value: "test", nested: { data: "nested" } }; // Test cached result cloning mockCache.has.mockReturnValue(true); mockCache.get.mockReturnValue(Promise.resolve(originalObject)); const cachedResult = await deduplicatedFn( undefined as never, undefined as never ); // Modify the returned result (cachedResult as any).value = "modified"; (cachedResult as any).nested.data = "modified-nested"; // Original should be unchanged (structuredClone protection) expect(originalObject.value).toBe("test"); expect(originalObject.nested.data).toBe("nested"); // Test fresh function result cloning mockCache.has.mockReturnValue(false); mockFn.mockResolvedValue(originalObject); const freshResult = await deduplicatedFn( undefined as never, undefined as never ); // Modify the returned result (freshResult as any).value = "modified-fresh"; // Original should be unchanged expect(originalObject.value).toBe("test"); }); it("should handle multiple concurrent requests to the same key", async () => { mockCache.has.mockReturnValueOnce(false).mockReturnValue(true); const sharedPromise = Promise.resolve("shared-result"); mockCache.get.mockReturnValue(sharedPromise); let resolveFunction: (value: string) => void; const delayedPromise = new Promise<string>((resolve) => { resolveFunction = resolve; }); mockFn.mockReturnValue(delayedPromise); // Start first request const firstRequest = deduplicatedFn( undefined as never, undefined as never ); // Start second request while first is pending const secondRequest = deduplicatedFn( undefined as never, undefined as never ); // The first request should set the cache expect(mockCache.set).toHaveBeenCalledWith("test-key", delayedPromise); // The second request should use the cached promise expect(mockFn).toHaveBeenCalledTimes(1); // Resolve the function resolveFunction?.("shared-result"); // Both should resolve to the same result expect(await firstRequest).toBe("shared-result"); expect(await secondRequest).toBe("shared-result"); }); }); describe("integration scenarios", () => { it("should work with different cache implementations", () => { const realCache = new Map<string, Promise<string>>(); const mockSyncedMap = { has: (key: string) => realCache.has(key), get: (key: string) => realCache.get(key), set: (key: string, value: Promise<string>) => { realCache.set(key, value); }, delete: (key: string) => { realCache.delete(key); }, }; const realHandler = new DeduplicatedRequestHandler( mockFn, 100, mockSyncedMap as any ); const deduplicatedFn = realHandler.deduplicatedFunction("real-key"); expect(deduplicatedFn).toBeInstanceOf(Function); }); it("should handle different return types", async () => { const objectFn = vi .fn<(...args: [never, never]) => Promise<{ data: string }>>() .mockResolvedValue({ data: "test-object" }); const objectHandler = new DeduplicatedRequestHandler( objectFn, 1000, mockCache ); const deduplicatedObjectFn = objectHandler.deduplicatedFunction("object-key"); mockCache.has.mockReturnValue(false); const result = await deduplicatedObjectFn( undefined as never, undefined as never ); expect(result).toEqual({ data: "test-object" }); const numberFn = vi .fn<(...args: [never, never]) => Promise<number>>() .mockResolvedValue(42); const numberHandler = new DeduplicatedRequestHandler( numberFn, 1000, mockCache ); const deduplicatedNumberFn = numberHandler.deduplicatedFunction("number-key"); const numberResult = await deduplicatedNumberFn( undefined as never, undefined as never ); expect(numberResult).toBe(42); }); }); });