UNPKG

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

Version:

Next.js redis cache handler

367 lines (299 loc) 10.7 kB
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import RedisStringsHandler from "../RedisStringsHandler"; import CachedHandler, { resetCachedHandler } from "../ZodHandler"; // Mock the RedisStringsHandler vi.mock("../RedisStringsHandler", () => ({ default: vi.fn().mockImplementation(() => ({ get: vi.fn(), set: vi.fn(), revalidateTag: vi.fn(), resetRequestCache: vi.fn(), })), })); describe("ZodHandler (Development CachedHandler)", () => { let handler: CachedHandler; let mockRedisHandler: any; beforeEach(() => { vi.clearAllMocks(); resetCachedHandler(); // Reset the singleton mockRedisHandler = { get: vi.fn(), set: vi.fn(), revalidateTag: vi.fn(), resetRequestCache: vi.fn(), }; (RedisStringsHandler as any).mockImplementation(() => mockRedisHandler); }); afterEach(() => { vi.restoreAllMocks(); }); describe("constructor", () => { it("should create a single RedisStringsHandler instance for development", () => { const options = { redisUrl: "redis://localhost:6379" }; handler = new CachedHandler(options); expect(RedisStringsHandler).toHaveBeenCalledTimes(1); expect(RedisStringsHandler).toHaveBeenCalledWith(options); // Creating another instance should not create a new RedisStringsHandler (singleton pattern) const _handler2 = new CachedHandler(options); expect(RedisStringsHandler).toHaveBeenCalledTimes(1); }); it("should log when development cached handler is created", () => { const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); handler = new CachedHandler({}); expect(consoleSpy).toHaveBeenCalledWith( "created development cached handler" ); consoleSpy.mockRestore(); }); }); describe("get method", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should delegate get calls to RedisStringsHandler with correct arguments", async () => { const key = "test-key"; const ctx = { kind: "APP_ROUTE" as const, isRoutePPREnabled: false, isFallback: false, }; const expectedResult = { value: "test", lastModified: Date.now(), tags: [], }; mockRedisHandler.get.mockResolvedValue(expectedResult); const result = await handler.get(key, ctx); expect(mockRedisHandler.get).toHaveBeenCalledWith(key, ctx); expect(result).toBe(expectedResult); }); it("should handle different context types for validation purposes", async () => { const contexts = [ { kind: "APP_ROUTE" as const, isRoutePPREnabled: false, isFallback: false, }, { kind: "APP_PAGE" as const, isRoutePPREnabled: true, isFallback: false, }, { kind: "FETCH" as const, revalidate: 60, fetchUrl: "https://example.com", fetchIdx: 0, tags: ["tag1"], softTags: ["tag2"], isFallback: false, }, ]; for (const ctx of contexts) { mockRedisHandler.get.mockResolvedValue(null); const result = await handler.get("test-key", ctx); expect(mockRedisHandler.get).toHaveBeenCalledWith("test-key", ctx); expect(result).toBeNull(); } expect(mockRedisHandler.get).toHaveBeenCalledTimes(3); }); }); describe("set method", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should delegate set calls to RedisStringsHandler with APP_ROUTE data", async () => { const key = "route-key"; const data = { kind: "APP_ROUTE" as const, status: 200, headers: { "cache-control": "public, max-age=60", "x-nextjs-stale-time": "1234567890", "x-next-cache-tags": "tag1,tag2", }, body: Buffer.from("response body"), }; const ctx = { isRoutePPREnabled: false, isFallback: false, revalidate: 60, }; mockRedisHandler.set.mockResolvedValue(undefined); await handler.set(key, data, ctx); expect(mockRedisHandler.set).toHaveBeenCalledWith(key, data, ctx); }); it("should validate and delegate APP_PAGE data types", async () => { const key = "page-key"; const data = { kind: "APP_PAGE" as const, status: 200, headers: { "x-nextjs-stale-time": "1234567890", "x-next-cache-tags": "page-tag", }, html: "<html>content</html>", rscData: Buffer.from("rsc data"), segmentData: { test: "data" }, postboned: { delayed: "content" }, }; const ctx = { isRoutePPREnabled: true, isFallback: false, tags: ["custom-tag"], cacheControl: { revalidate: 5, expire: undefined }, }; mockRedisHandler.set.mockResolvedValue(undefined); await handler.set(key, data, ctx); expect(mockRedisHandler.set).toHaveBeenCalledWith(key, data, ctx); }); it("should validate and delegate FETCH data types", async () => { const key = "fetch-key"; const data = { kind: "FETCH" as const, data: { headers: { "content-type": "application/json" }, body: "YmFzZTY0ZGF0YQ==", // base64 encoded data status: 200, url: "https://api.example.com/data", }, revalidate: 300, }; const ctx = { isRoutePPREnabled: false, isFallback: false, revalidate: false, }; mockRedisHandler.set.mockResolvedValue(undefined); await handler.set(key, data, ctx); expect(mockRedisHandler.set).toHaveBeenCalledWith(key, data, ctx); }); }); describe("revalidateTag method", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should delegate revalidateTag calls with single tag", async () => { const tag = "test-tag"; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tag); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith(tag); }); it("should delegate revalidateTag calls with multiple tags", async () => { const tags = ["tag1", "tag2", "tag3"]; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tags); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith(tags); }); it("should handle revalidateTag with additional arguments for validation", async () => { const tag = "validation-tag"; const additionalArg1 = "extra-validation-data"; const additionalArg2 = { validation: "object" }; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tag, additionalArg1, additionalArg2); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith( tag, additionalArg1, additionalArg2 ); }); }); describe("resetRequestCache method", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should delegate resetRequestCache calls", () => { mockRedisHandler.resetRequestCache.mockReturnValue(undefined); const result = handler.resetRequestCache(); expect(mockRedisHandler.resetRequestCache).toHaveBeenCalledWith(); expect(result).toBeUndefined(); }); it("should handle resetRequestCache with arguments", () => { const arg1 = "cache-reset-arg"; const arg2 = { reset: "options" }; mockRedisHandler.resetRequestCache.mockReturnValue(undefined); const result = handler.resetRequestCache(arg1, arg2); expect(mockRedisHandler.resetRequestCache).toHaveBeenCalledWith( arg1, arg2 ); expect(result).toBeUndefined(); }); }); describe("development validation behavior", () => { it("should maintain the same singleton pattern as production handler", () => { const handler1 = new CachedHandler({ redisUrl: "redis://dev:6379" }); const handler2 = new CachedHandler({ redisUrl: "redis://staging:6379" }); const handler3 = new CachedHandler({}); // Only one RedisStringsHandler should be created regardless of different options expect(RedisStringsHandler).toHaveBeenCalledTimes(1); // All handlers should use the same underlying Redis handler for development consistency const mockGet = vi.fn(); mockRedisHandler.get = mockGet; handler1.get("dev-key", { kind: "APP_ROUTE", isRoutePPREnabled: false, isFallback: false, }); handler2.get("staging-key", { kind: "APP_PAGE", isRoutePPREnabled: true, isFallback: false, }); handler3.get("local-key", { kind: "FETCH", revalidate: 60, fetchUrl: "test", fetchIdx: 0, tags: [], softTags: [], isFallback: false, }); expect(mockGet).toHaveBeenCalledTimes(3); }); it("should provide the same interface as the production CachedHandler", () => { handler = new CachedHandler({}); // Verify all expected methods are available expect(typeof handler.get).toBe("function"); expect(typeof handler.set).toBe("function"); expect(typeof handler.revalidateTag).toBe("function"); expect(typeof handler.resetRequestCache).toBe("function"); }); it("should handle error scenarios for validation testing", async () => { handler = new CachedHandler({}); // Test error propagation const error = new Error("Validation error"); mockRedisHandler.get.mockRejectedValue(error); await expect( handler.get("error-key", { kind: "APP_ROUTE", isRoutePPREnabled: false, isFallback: false, }) ).rejects.toThrow("Validation error"); mockRedisHandler.set.mockRejectedValue(error); await expect( handler.set( "error-key", { kind: "APP_ROUTE", status: 500, headers: { "x-nextjs-stale-time": "1234567890", "x-next-cache-tags": "error-tag", }, body: Buffer.from("error"), }, { isRoutePPREnabled: false, isFallback: false, } ) ).rejects.toThrow("Validation error"); mockRedisHandler.revalidateTag.mockRejectedValue(error); await expect(handler.revalidateTag("error-tag")).rejects.toThrow( "Validation error" ); }); }); });