plan/react-rewrite #1
@@ -0,0 +1,143 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { healthMiddleware } from "./health.js";
|
||||
import type { ApiClient } from "@/shared/api/client.js";
|
||||
|
||||
function createMockApiClient(
|
||||
behavior: "ok" | "error" | "timeout" = "ok",
|
||||
): ApiClient {
|
||||
const client = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
} as unknown as ApiClient;
|
||||
|
||||
if (behavior === "ok") {
|
||||
vi.mocked(client.get).mockResolvedValue({ status: "ok" });
|
||||
} else if (behavior === "error") {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error("upstream down"));
|
||||
} else {
|
||||
vi.mocked(client.get).mockImplementation(
|
||||
() => new Promise(() => {}), // never resolves
|
||||
);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
function createMockRes() {
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn().mockReturnThis(),
|
||||
setHeader: vi.fn(),
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
describe("healthMiddleware", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("returns 200 when upstream is reachable", async () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
|
||||
});
|
||||
|
||||
it("returns 503 when upstream ping fails", async () => {
|
||||
const apiClient = createMockApiClient("error");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
status: "degraded",
|
||||
reason: "upstream_unreachable",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 200 if last success is within 60s even if current ping fails", async () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const res1 = createMockRes();
|
||||
await handler(req, res1 as any, vi.fn());
|
||||
expect(res1.status).toHaveBeenCalledWith(200);
|
||||
|
||||
// Now make it fail
|
||||
vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));
|
||||
|
||||
// Advance 30s — still within 60s window
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
const res2 = createMockRes();
|
||||
await handler(req, res2 as any, vi.fn());
|
||||
expect(res2.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it("returns 503 if last success is older than 60s", async () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const res1 = createMockRes();
|
||||
await handler(req, res1 as any, vi.fn());
|
||||
expect(res1.status).toHaveBeenCalledWith(200);
|
||||
|
||||
// Now make it fail
|
||||
vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));
|
||||
|
||||
// Advance 61s — beyond 60s window
|
||||
vi.advanceTimersByTime(61_000);
|
||||
|
||||
const res2 = createMockRes();
|
||||
await handler(req, res2 as any, vi.fn());
|
||||
expect(res2.status).toHaveBeenCalledWith(503);
|
||||
});
|
||||
|
||||
it("respects custom upstreamTimeoutMs", async () => {
|
||||
const apiClient = createMockApiClient("timeout");
|
||||
const handler = healthMiddleware({
|
||||
apiClient,
|
||||
upstreamTimeoutMs: 100,
|
||||
});
|
||||
|
||||
const req = {} as any;
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
// The handler should abort the ping after 100ms
|
||||
const promise = handler(req, res as any, next);
|
||||
vi.advanceTimersByTime(200);
|
||||
await promise;
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503);
|
||||
});
|
||||
|
||||
it("does not call next — health is a terminal response", async () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ApiClient } from "@/shared/api/client.js";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
export interface HealthMiddlewareOptions {
|
||||
apiClient: ApiClient;
|
||||
upstreamTimeoutMs?: number;
|
||||
}
|
||||
|
||||
const STALE_THRESHOLD_MS = 60_000;
|
||||
const DEFAULT_UPSTREAM_TIMEOUT_MS = 5_000;
|
||||
|
||||
/**
|
||||
* Factory that creates a health-check handler. Returns 200 if the last
|
||||
* successful upstream ping is within 60 s, 503 otherwise.
|
||||
*
|
||||
* This is an Express-style `(req, res, next)` handler — it does NOT
|
||||
* auto-register itself as Modern.js middleware. Registration happens
|
||||
* in a future integration step.
|
||||
*/
|
||||
export function healthMiddleware(options: HealthMiddlewareOptions) {
|
||||
const { apiClient, upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS } =
|
||||
options;
|
||||
|
||||
let lastSuccessTs = 0;
|
||||
|
||||
return async (
|
||||
_req: IncomingMessage,
|
||||
res: ServerResponse & {
|
||||
status: (code: number) => ServerResponse;
|
||||
json: (body: unknown) => void;
|
||||
},
|
||||
_next: () => void,
|
||||
): Promise<void> => {
|
||||
// Attempt to ping the upstream API
|
||||
try {
|
||||
await Promise.race([
|
||||
apiClient.get("/health"),
|
||||
new Promise<never>((_resolve, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("upstream_timeout")),
|
||||
upstreamTimeoutMs,
|
||||
),
|
||||
),
|
||||
]);
|
||||
lastSuccessTs = Date.now();
|
||||
} catch {
|
||||
// ping failed — rely on cached lastSuccessTs
|
||||
}
|
||||
|
||||
const age = Date.now() - lastSuccessTs;
|
||||
|
||||
if (lastSuccessTs > 0 && age < STALE_THRESHOLD_MS) {
|
||||
res.status(200).json({ status: "ok" });
|
||||
} else {
|
||||
res
|
||||
.status(503)
|
||||
.json({ status: "degraded", reason: "upstream_unreachable" });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user