UNPKG

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

Version:

Next.js redis cache handler

354 lines (287 loc) 10.1 kB
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import CachedHandler, { resetCachedHandler } from "../CachedHandler"; import RedisStringsHandler from "../RedisStringsHandler"; // Mock the RedisStringsHandler vi.mock("../RedisStringsHandler", () => ({ default: vi.fn().mockImplementation(() => ({ get: vi.fn(), set: vi.fn(), revalidateTag: vi.fn(), resetRequestCache: vi.fn(), })), })); describe("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", () => { 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 const _handler2 = new CachedHandler(options); expect(RedisStringsHandler).toHaveBeenCalledTimes(1); }); it("should log when cached handler is created", () => { const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); handler = new CachedHandler({}); expect(consoleSpy).toHaveBeenCalledWith("created cached handler"); consoleSpy.mockRestore(); }); }); describe("get", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should call RedisStringsHandler.get 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 get with FETCH context", async () => { const key = "fetch-key"; const ctx = { kind: "FETCH" as const, revalidate: 60, fetchUrl: "https://example.com", fetchIdx: 0, tags: ["tag1"], softTags: ["tag2"], isFallback: false, }; mockRedisHandler.get.mockResolvedValue(null); const result = await handler.get(key, ctx); expect(mockRedisHandler.get).toHaveBeenCalledWith(key, ctx); expect(result).toBeNull(); }); it("should handle get with APP_PAGE context", async () => { const key = "page-key"; const ctx = { kind: "APP_PAGE" as const, isRoutePPREnabled: true, isFallback: false, }; const expectedResult = { value: "page-data", lastModified: Date.now(), tags: ["page-tag"], }; mockRedisHandler.get.mockResolvedValue(expectedResult); const result = await handler.get(key, ctx); expect(mockRedisHandler.get).toHaveBeenCalledWith(key, ctx); expect(result).toBe(expectedResult); }); }); describe("set", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should call RedisStringsHandler.set 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 call RedisStringsHandler.set with APP_PAGE data", 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: {}, postboned: null, }; const ctx = { isRoutePPREnabled: true, isFallback: false, tags: ["custom-tag"], }; mockRedisHandler.set.mockResolvedValue(undefined); await handler.set(key, data, ctx); expect(mockRedisHandler.set).toHaveBeenCalledWith(key, data, ctx); }); it("should call RedisStringsHandler.set with FETCH data", async () => { const key = "fetch-key"; const data = { kind: "FETCH" as const, data: { headers: { "content-type": "application/json" }, body: "base64data", status: 200, url: "https://example.com", }, revalidate: 300, }; const ctx = { isRoutePPREnabled: false, isFallback: false, }; mockRedisHandler.set.mockResolvedValue(undefined); await handler.set(key, data, ctx); expect(mockRedisHandler.set).toHaveBeenCalledWith(key, data, ctx); }); it("should propagate errors from RedisStringsHandler.set", async () => { const key = "error-key"; const data = { kind: "APP_ROUTE" as const, status: 500, headers: { "x-nextjs-stale-time": "1234567890", "x-next-cache-tags": "error-tag", }, body: Buffer.from("error"), }; const ctx = { isRoutePPREnabled: false, isFallback: false, }; const error = new Error("Redis connection failed"); mockRedisHandler.set.mockRejectedValue(error); await expect(handler.set(key, data, ctx)).rejects.toThrow( "Redis connection failed" ); }); }); describe("revalidateTag", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should call RedisStringsHandler.revalidateTag with single tag", async () => { const tag = "test-tag"; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tag); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith(tag); }); it("should call RedisStringsHandler.revalidateTag with multiple tags", async () => { const tags = ["tag1", "tag2", "tag3"]; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tags); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith(tags); }); it("should call RedisStringsHandler.revalidateTag with additional arguments", async () => { const tag = "test-tag"; const additionalArg1 = "extra1"; const additionalArg2 = "extra2"; mockRedisHandler.revalidateTag.mockResolvedValue(undefined); await handler.revalidateTag(tag, additionalArg1, additionalArg2); expect(mockRedisHandler.revalidateTag).toHaveBeenCalledWith( tag, additionalArg1, additionalArg2 ); }); it("should propagate errors from RedisStringsHandler.revalidateTag", async () => { const tag = "error-tag"; const error = new Error("Revalidation failed"); mockRedisHandler.revalidateTag.mockRejectedValue(error); await expect(handler.revalidateTag(tag)).rejects.toThrow( "Revalidation failed" ); }); }); describe("resetRequestCache", () => { beforeEach(() => { handler = new CachedHandler({}); }); it("should call RedisStringsHandler.resetRequestCache", () => { mockRedisHandler.resetRequestCache.mockReturnValue(undefined); const result = handler.resetRequestCache(); expect(mockRedisHandler.resetRequestCache).toHaveBeenCalledWith(); expect(result).toBeUndefined(); }); it("should call RedisStringsHandler.resetRequestCache with arguments", () => { const arg1 = "test-arg"; const arg2 = { test: "object" }; mockRedisHandler.resetRequestCache.mockReturnValue(undefined); const result = handler.resetRequestCache(arg1, arg2); expect(mockRedisHandler.resetRequestCache).toHaveBeenCalledWith( arg1, arg2 ); expect(result).toBeUndefined(); }); }); describe("singleton behavior", () => { it("should reuse the same RedisStringsHandler instance across multiple CachedHandler instances", () => { const handler1 = new CachedHandler({ redisUrl: "redis://localhost:6379", }); const handler2 = new CachedHandler({ redisUrl: "redis://localhost:6380", }); // Different options 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 const mockGet = vi.fn(); mockRedisHandler.get = mockGet; handler1.get("key1", { kind: "APP_ROUTE", isRoutePPREnabled: false, isFallback: false, }); handler2.get("key2", { kind: "APP_PAGE", isRoutePPREnabled: true, isFallback: false, }); handler3.get("key3", { kind: "FETCH", revalidate: 60, fetchUrl: "test", fetchIdx: 0, tags: [], softTags: [], isFallback: false, }); expect(mockGet).toHaveBeenCalledTimes(3); }); }); });