fix : correct api healthz
CI / Verify (push) Successful in 1m45s
CI / Build immutable images and deploy (push) Successful in 2m24s

This commit is contained in:
2026-10-02 18:57:43 +07:00 Unverified
parent df8c89914e
commit f5b432a496
11 changed files with 193 additions and 50 deletions
+54
View File
@@ -0,0 +1,54 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const exists = vi.fn();
vi.mock("@/lib/redis/client", () => ({
getRedisClient: async () => ({ exists }),
discordWorkerHeartbeatKey: () => "buzz:events:discord-worker:heartbeat",
}));
const { checkSlip2Go, checkDiscordBot, checkLunaris } = await import("./services");
const originalFetch = globalThis.fetch;
const originalUrl = process.env.SLIP2GO_VERIFY_URL;
const originalSecret = process.env.SLIP2GO_API_SECRET;
const fetchMock = vi.fn();
describe("external service checks", () => {
beforeEach(() => {
vi.clearAllMocks();
globalThis.fetch = fetchMock as unknown as typeof fetch;
process.env.SLIP2GO_VERIFY_URL = "https://slip2go.example.test/verify";
process.env.SLIP2GO_API_SECRET = "secret";
});
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalUrl === undefined) delete process.env.SLIP2GO_VERIFY_URL;
else process.env.SLIP2GO_VERIFY_URL = originalUrl;
if (originalSecret === undefined) delete process.env.SLIP2GO_API_SECRET;
else process.env.SLIP2GO_API_SECRET = originalSecret;
});
it("checks Slip2Go reachability without submitting a slip", async () => {
fetchMock.mockResolvedValue(new Response(null, { status: 405 }));
await expect(checkSlip2Go(AbortSignal.timeout(1000))).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenCalledWith(
new URL("https://slip2go.example.test/verify"),
expect.objectContaining({ method: "HEAD", cache: "no-store" }),
);
fetchMock.mockResolvedValue(new Response(null, { status: 503 }));
await expect(checkSlip2Go(AbortSignal.timeout(1000))).rejects.toThrow("503");
});
it("requires a valid Lunaris version response", async () => {
fetchMock.mockResolvedValueOnce(Response.json({ version: "7.0.54" }));
await expect(checkLunaris(AbortSignal.timeout(1000))).resolves.toBeUndefined();
fetchMock.mockResolvedValueOnce(Response.json({ version: "invalid" }));
await expect(checkLunaris(AbortSignal.timeout(1000))).rejects.toThrow();
});
it("requires a live Discord worker heartbeat", async () => {
exists.mockResolvedValueOnce(1).mockResolvedValueOnce(0);
await expect(checkDiscordBot()).resolves.toBeUndefined();
await expect(checkDiscordBot()).rejects.toThrow("heartbeat");
});
});
+31
View File
@@ -0,0 +1,31 @@
import { lunarisVersionSchema } from "@/lib/catalog/version";
import { discordWorkerHeartbeatKey, getRedisClient } from "@/lib/redis/client";
export async function checkSlip2Go(signal: AbortSignal): Promise<void> {
const configuredUrl = process.env.SLIP2GO_VERIFY_URL;
if (!configuredUrl || !process.env.SLIP2GO_API_SECRET) throw new Error("Slip2Go is not configured.");
const url = new URL(configuredUrl);
if (url.protocol !== "https:") throw new Error("Slip2Go URL must use HTTPS.");
const response = await fetch(url, { method: "HEAD", cache: "no-store", signal });
// The verification endpoint requires a real slip, so a rejected HEAD request can still prove reachability.
if (!response.ok && ![400, 401, 403, 405, 415, 422].includes(response.status)) {
throw new Error(`Slip2Go returned ${response.status}.`);
}
}
export async function checkLunaris(signal: AbortSignal): Promise<void> {
const response = await fetch("https://api.lunaris.moe/data/version.json", { cache: "no-store", signal });
if (!response.ok) throw new Error(`Lunaris returned ${response.status}.`);
const body: unknown = await response.json();
if (typeof body !== "object" || body === null || !("version" in body)) {
throw new Error("Invalid Lunaris version response.");
}
lunarisVersionSchema.parse(body.version);
}
export async function checkDiscordBot(): Promise<void> {
const redis = await getRedisClient();
if (!(await redis.exists(discordWorkerHeartbeatKey()))) {
throw new Error("Discord worker heartbeat is missing.");
}
}
+4
View File
@@ -88,3 +88,7 @@ export function redisEventPrefix(): string {
export function redisEventChannel(topic: string): string {
return `${redisEventPrefix()}:${topic}`;
}
export function discordWorkerHeartbeatKey(): string {
return `${redisEventPrefix()}:discord-worker:heartbeat`;
}