Files
cfw-autumn/server/tests/unit/cache/cache-utils.test.ts
2026-05-28 11:53:49 +01:00

154 lines
4.0 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
const mockState = {
warnings: [] as Array<{ message: string; data?: Record<string, unknown> }>,
};
const defaultRedis = { status: "ready" };
// Stub the full `Logger` shape — Bun's `mock.module` is process-wide, so
// later unit tests inherit this stub. Missing methods crash unrelated code.
const mockLogger = {
debug: () => undefined,
info: () => undefined,
warn: (data: Record<string, unknown> | string, message?: string) => {
if (typeof data === "string") {
mockState.warnings.push({ message: data });
return;
}
mockState.warnings.push({
message: message || "",
data,
});
},
error: () => undefined,
child: () => mockLogger,
};
mock.module("@/external/logtail/logtailUtils.js", () => ({
logger: mockLogger,
}));
mock.module("@/external/redis/initUtils/redisConfig.js", () => ({
hasRedisConfig: true,
}));
mock.module("@/external/redis/initUtils/redisClientRegistry.js", () => ({
redis: defaultRedis,
}));
mock.module("@/external/redis/initRedis.js", () => ({
redis: defaultRedis,
}));
import { RedisUnavailableError } from "@/external/redis/utils/errors.js";
import {
tryRedisNx,
tryRedisRead,
tryRedisWrite,
} from "@/utils/cacheUtils/cacheUtils.js";
describe("cache utils", () => {
beforeEach(() => {
mockState.warnings = [];
});
test("tryRedisRead returns null and warns when Redis operation throws", async () => {
const result = await tryRedisRead(
async () => {
throw new Error("boom");
},
{ status: "ready" } as never,
);
expect(result).toBeNull();
expect(mockState.warnings).toHaveLength(1);
expect(mockState.warnings[0]).toEqual({
message: "[redis] operation unavailable",
data: {
source: "tryRedisRead:error",
error: "boom",
},
});
});
test("tryRedisWrite throws RedisUnavailableError when Redis is not ready", async () => {
const error = await tryRedisWrite(async () => "OK", {
status: "connecting",
} as never).catch((caught) => caught);
expect(error).toBeInstanceOf(RedisUnavailableError);
expect(error.reason).toBe("not_ready");
expect(mockState.warnings).toHaveLength(1);
expect(mockState.warnings[0]).toEqual({
message: "[redis] operation unavailable",
data: {
source: "tryRedisWrite:not-ready",
error: undefined,
},
});
});
test("tryRedisRead throws RedisUnavailableError when Redis command times out", async () => {
const error = await tryRedisRead(
async () => {
throw new Error("Command timed out");
},
{ status: "ready" } as never,
).catch((caught) => caught);
expect(error).toBeInstanceOf(RedisUnavailableError);
expect(error.reason).toBe("timeout");
});
test("tryRedisNx throws RedisUnavailableError on connection failures", async () => {
const error = await tryRedisNx({
operation: async () => {
throw new Error("Connection is closed.");
},
redisInstance: { status: "ready" } as never,
onRedisUnavailable: () => "fallback",
onSuccess: () => "success",
onKeyAlreadyExists: () => "exists",
}).catch((caught) => caught);
expect(error).toBeInstanceOf(RedisUnavailableError);
expect(error.reason).toBe("connection");
});
test("tryRedisNx falls back and warns when Redis operation throws", async () => {
const result = await tryRedisNx({
operation: async () => {
throw new Error("boom");
},
redisInstance: { status: "ready" } as never,
onRedisUnavailable: () => "fallback",
onSuccess: () => "success",
onKeyAlreadyExists: () => "exists",
});
expect(result).toBe("fallback");
});
test("custom Redis failures do not mark default Redis unavailable", async () => {
const result = await tryRedisRead(
async () => {
throw new Error("custom redis failed");
},
{ status: "ready" } as never,
);
expect(result).toBeNull();
});
test("Redis command errors do not mark Redis unavailable", async () => {
await tryRedisWrite(async () => {
throw new Error("ERR user_script:2: unexpected symbol near '#'");
});
expect(mockState.warnings).toHaveLength(1);
});
});
afterAll(() => {
mock.restore();
});