feat: add extensive test suite

This commit is contained in:
2026-08-30 11:33:53 +07:00 Unverified
parent 061cce00dc
commit 8359c7272e
131 changed files with 8717 additions and 2200 deletions
+125
View File
@@ -0,0 +1,125 @@
import { describe, expect, mock, test } from "bun:test";
import { createHealthHandler, type HealthCheck } from "./handler";
function redisMock() {
return {
ping: mock(() => Promise.resolve("PONG")),
exists: mock(() => Promise.resolve(0)),
get: mock((): Promise<string | null> => Promise.resolve(null)),
setex: mock(() => Promise.resolve("OK")),
};
}
describe("health handler", () => {
test("returns 200 and caches successful checks", async () => {
const redis = redisMock();
const checks = {
database: mock(() => Promise.resolve(true)),
upstream: mock(() =>
Promise.resolve(new Response(null, { status: 204 })),
),
};
const response = await createHealthHandler({ redis, checks })();
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
red: true,
database: true,
upstream: true,
});
expect(redis.setex).toHaveBeenCalledTimes(2);
expect(redis.setex).toHaveBeenCalledWith("health:database", 900, "ok");
});
test("uses cached health without running a dependency", async () => {
const redis = redisMock();
redis.exists.mockResolvedValue(1);
redis.get.mockResolvedValue("ok");
const database = mock(() => Promise.resolve(false));
const response = await createHealthHandler({
redis,
checks: { database },
})();
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ red: true, database: true });
expect(database).not.toHaveBeenCalled();
});
test("treats a non-ok cached value as false", async () => {
const redis = redisMock();
redis.exists.mockResolvedValue(1);
redis.get.mockResolvedValue("failed");
const response = await createHealthHandler({
redis,
checks: { database: mock(() => Promise.resolve(true)) },
})();
expect(response.status).toBe(503);
expect(await response.json()).toEqual({ red: true, database: false });
});
test("converts rejected checks to false without rejecting the request", async () => {
const checks: Record<string, HealthCheck> = {
database: () => Promise.resolve(true),
rejected: () => Promise.reject(new Error("offline")),
badResponse: () => Promise.resolve(new Response(null, { status: 500 })),
falseValue: () => Promise.resolve(false),
};
const response = await createHealthHandler({
redis: redisMock(),
checks,
})();
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
red: true,
database: true,
rejected: false,
badResponse: false,
falseValue: false,
});
});
test("returns 503 when Redis is unavailable and still executes checks", async () => {
const redis = redisMock();
redis.ping.mockRejectedValue(new Error("offline"));
const database = mock(() => Promise.resolve(true));
const response = await createHealthHandler({
redis,
checks: { database },
})();
expect(response.status).toBe(503);
expect(await response.json()).toEqual({ red: false, database: true });
expect(database).toHaveBeenCalled();
expect(redis.exists).not.toHaveBeenCalled();
expect(redis.setex).not.toHaveBeenCalled();
});
test("returns 503 when the database is unhealthy", async () => {
const response = await createHealthHandler({
redis: redisMock(),
checks: { database: () => Promise.resolve(false) },
})();
expect(response.status).toBe(503);
});
test("falls back to a live check when reading the cache rejects", async () => {
const redis = redisMock();
redis.exists.mockRejectedValue(new Error("cache failed"));
const database = mock(() => Promise.resolve(true));
const response = await createHealthHandler({
redis,
checks: { database },
})();
expect(response.status).toBe(200);
expect(database).toHaveBeenCalled();
});
test("a cache write rejection does not change a healthy result", async () => {
const redis = redisMock();
redis.setex.mockRejectedValue(new Error("readonly"));
const response = await createHealthHandler({
redis,
checks: { database: () => Promise.resolve(true) },
})();
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ red: true, database: true });
});
});
+57
View File
@@ -0,0 +1,57 @@
export type HealthCheck = () => Promise<Response | boolean>;
type HealthRedis = {
ping(): Promise<unknown>;
exists(key: string): Promise<number | boolean>;
get(key: string): Promise<string | null>;
setex(key: string, ttl: number, value: string): Promise<unknown>;
};
const checkResult = async (check: HealthCheck) => {
try {
const result = await check();
return result instanceof Response ? result.ok : !!result;
} catch {
return false;
}
};
export function createHealthHandler({
redis,
checks,
}: {
redis: HealthRedis;
checks: Record<string, HealthCheck>;
}) {
return async function GET() {
const red = await redis
.ping()
.then(() => true)
.catch(() => false);
const entries = await Promise.all(
Object.entries(checks).map(async ([name, check]) => {
const key = `health:${name}`;
if (red) {
try {
if (await redis.exists(key))
return [name, (await redis.get(key)) === "ok"] as const;
} catch {}
}
const healthy = await checkResult(check);
if (red && healthy) {
try {
await redis.setex(key, 900, "ok");
} catch {}
}
return [name, healthy] as const;
}),
);
const results = Object.fromEntries(entries) as Record<string, boolean>;
return Response.json(
{ red, ...results },
{ status: red && results.database ? 200 : 503 },
);
};
}
+9 -35
View File
@@ -1,46 +1,16 @@
import { GoogleGenAI } from "@google/genai";
import { env } from "bun";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { redis as redisShared } from "@/lib/db/redis";
import { GoogleGenAI } from "@google/genai";
import { env } from "bun";
import { createHealthHandler } from "./handler";
const { DISCORD_BOT_TOKEN, GEMINI_TTS_API_KEY } = env as Record<string, string>;
const redis = redisShared!;
export const revalidate = 60;
export async function GET() {
const red = await redis
.ping()
.then(() => true)
.catch(() => false);
const res = Object.fromEntries(
await Promise.all(
Object.entries(checks).map(
async ([k, v]) =>
[
k,
red
? (await redis.exists(`health:${k}`))
? (await redis.get(`health:${k}`)) === "ok"
: await v().then(async (r: any) => {
const v = r instanceof Response ? r.ok : !!r;
if (v) await redis.setex(`health:${k}`, 900, "ok");
return v;
})
: await v().then((r: any) =>
r instanceof Response ? r.ok : !!r,
),
] as const,
),
),
);
return Response.json(
{ red, ...res },
{ status: res.database && red ? 200 : 201 },
);
}
const checks = {
async database() {
return await db
@@ -76,8 +46,10 @@ const checks = {
});
},
async tts() {
const apiKey = GEMINI_TTS_API_KEY?.split(",").find(Boolean);
if (!apiKey) return false;
const client = new GoogleGenAI({
apiKey: GEMINI_TTS_API_KEY.split(",").pop(),
apiKey,
});
return client.models
.list()
@@ -98,3 +70,5 @@ const checks = {
});
},
} satisfies Record<string, () => Promise<Response | boolean>>;
export const GET = createHealthHandler({ redis, checks });