mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-23 13:48:58 -05:00
chore: introduce new reliable cache for enterprise license check (#5740)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import { createCache } from "cache-manager";
|
||||
import { Keyv } from "keyv";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("keyv");
|
||||
vi.mock("@keyv/redis");
|
||||
vi.mock("cache-manager");
|
||||
vi.mock("@formbricks/logger");
|
||||
|
||||
const mockCacheInstance = {
|
||||
set: vi.fn(),
|
||||
get: vi.fn(),
|
||||
del: vi.fn(),
|
||||
};
|
||||
|
||||
const CACHE_TTL_SECONDS = 60 * 60 * 24; // 24 hours
|
||||
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
|
||||
|
||||
describe("Cache Service", () => {
|
||||
let originalRedisUrl: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalRedisUrl = process.env.REDIS_URL;
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules(); // Crucial for re-running module initialization logic
|
||||
|
||||
// Setup default mock implementations
|
||||
vi.mocked(createCache).mockReturnValue(mockCacheInstance as any);
|
||||
vi.mocked(Keyv).mockClear(); // Clear any previous calls
|
||||
vi.mocked(KeyvRedis).mockClear(); // Clear any previous calls
|
||||
vi.mocked(logger.warn).mockClear(); // Clear logger warnings
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.REDIS_URL = originalRedisUrl;
|
||||
});
|
||||
|
||||
describe("Initialization and getCache", () => {
|
||||
test("should use Redis store and return it via getCache if REDIS_URL is set", async () => {
|
||||
process.env.REDIS_URL = "redis://localhost:6379";
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith("redis://localhost:6379");
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
store: expect.any(KeyvRedis),
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith("Successfully connected to Redis cache");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should fall back to memory store if Redis connection fails", async () => {
|
||||
process.env.REDIS_URL = "redis://localhost:6379";
|
||||
const mockError = new Error("Connection refused");
|
||||
vi.mocked(KeyvRedis).mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith("redis://localhost:6379");
|
||||
expect(logger.error).toHaveBeenCalledWith("Failed to connect to Redis cache:", mockError);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"Falling back to in-memory cache due to Redis connection failure"
|
||||
);
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should use memory store, log warning, and return it via getCache if REDIS_URL is not set", async () => {
|
||||
delete process.env.REDIS_URL;
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).not.toHaveBeenCalled();
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith("REDIS_URL not found, falling back to in-memory cache.");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should use memory store, log warning, and return it via getCache if REDIS_URL is an empty string", async () => {
|
||||
process.env.REDIS_URL = ""; // Test with empty string
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
// If REDIS_URL is "", it's falsy, so it should fall back to memory store
|
||||
expect(KeyvRedis).not.toHaveBeenCalled();
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS, // Expect memory store configuration
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith("REDIS_URL not found, falling back to in-memory cache.");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import "server-only";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import { type Cache, createCache } from "cache-manager";
|
||||
import { Keyv } from "keyv";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const CACHE_TTL_SECONDS = 60 * 60 * 24; // 24 hours
|
||||
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
|
||||
|
||||
let cache: Cache;
|
||||
|
||||
const initializeMemoryCache = (): void => {
|
||||
const memoryKeyvStore = new Keyv({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
cache = createCache({
|
||||
stores: [memoryKeyvStore],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
logger.info("Using in-memory cache");
|
||||
};
|
||||
|
||||
if (process.env.REDIS_URL) {
|
||||
try {
|
||||
const redisStore = new KeyvRedis(process.env.REDIS_URL);
|
||||
|
||||
// Gracefully fall back if Redis dies later on
|
||||
redisStore.on("error", (err) => {
|
||||
logger.error("Redis connection lost – switching to in-memory cache", { error: err });
|
||||
initializeMemoryCache();
|
||||
});
|
||||
|
||||
const redisKeyvStore = new Keyv({
|
||||
store: redisStore,
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
|
||||
cache = createCache({
|
||||
stores: [redisKeyvStore],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
logger.info("Successfully connected to Redis cache");
|
||||
} catch (error) {
|
||||
logger.error("Failed to connect to Redis cache:", error);
|
||||
logger.warn("Falling back to in-memory cache due to Redis connection failure");
|
||||
initializeMemoryCache();
|
||||
}
|
||||
} else {
|
||||
logger.warn("REDIS_URL not found, falling back to in-memory cache.");
|
||||
initializeMemoryCache();
|
||||
}
|
||||
|
||||
export const getCache = (): Cache => {
|
||||
return cache;
|
||||
};
|
||||
Reference in New Issue
Block a user