plan/react-rewrite #1
@@ -33,6 +33,10 @@ function createMockRes() {
|
||||
return res;
|
||||
}
|
||||
|
||||
function createRequest() {
|
||||
return {} as Parameters<ReturnType<typeof healthMiddleware>>[0];
|
||||
}
|
||||
|
||||
describe("healthMiddleware", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -42,11 +46,11 @@ describe("healthMiddleware", () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
|
||||
@@ -56,11 +60,11 @@ describe("healthMiddleware", () => {
|
||||
const apiClient = createMockApiClient("error");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
@@ -73,9 +77,9 @@ describe("healthMiddleware", () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res1 = createMockRes();
|
||||
await handler(req, res1 as any, vi.fn());
|
||||
await handler(req, res1, vi.fn());
|
||||
expect(res1.status).toHaveBeenCalledWith(200);
|
||||
|
||||
// Now make it fail
|
||||
@@ -85,7 +89,7 @@ describe("healthMiddleware", () => {
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
const res2 = createMockRes();
|
||||
await handler(req, res2 as any, vi.fn());
|
||||
await handler(req, res2, vi.fn());
|
||||
expect(res2.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
@@ -93,9 +97,9 @@ describe("healthMiddleware", () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res1 = createMockRes();
|
||||
await handler(req, res1 as any, vi.fn());
|
||||
await handler(req, res1, vi.fn());
|
||||
expect(res1.status).toHaveBeenCalledWith(200);
|
||||
|
||||
// Now make it fail
|
||||
@@ -105,7 +109,7 @@ describe("healthMiddleware", () => {
|
||||
vi.advanceTimersByTime(61_000);
|
||||
|
||||
const res2 = createMockRes();
|
||||
await handler(req, res2 as any, vi.fn());
|
||||
await handler(req, res2, vi.fn());
|
||||
expect(res2.status).toHaveBeenCalledWith(503);
|
||||
});
|
||||
|
||||
@@ -116,12 +120,12 @@ describe("healthMiddleware", () => {
|
||||
upstreamTimeoutMs: 100,
|
||||
});
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
// The handler should abort the ping after 100ms
|
||||
const promise = handler(req, res as any, next);
|
||||
const promise = handler(req, res, next);
|
||||
vi.advanceTimersByTime(200);
|
||||
await promise;
|
||||
|
||||
@@ -132,11 +136,11 @@ describe("healthMiddleware", () => {
|
||||
const apiClient = createMockApiClient("ok");
|
||||
const handler = healthMiddleware({ apiClient });
|
||||
|
||||
const req = {} as any;
|
||||
const req = createRequest();
|
||||
const res = createMockRes();
|
||||
const next = vi.fn();
|
||||
|
||||
await handler(req, res as any, next);
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { ApiClient } from "@/shared/api/client.js";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
export interface HealthMiddlewareOptions {
|
||||
apiClient: ApiClient;
|
||||
upstreamTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/** Express-style response with status/json helpers. */
|
||||
interface HealthResponse {
|
||||
status(code: number): HealthResponse;
|
||||
json(body: unknown): void;
|
||||
}
|
||||
|
||||
const STALE_THRESHOLD_MS = 60_000;
|
||||
const DEFAULT_UPSTREAM_TIMEOUT_MS = 5_000;
|
||||
|
||||
@@ -24,11 +29,8 @@ export function healthMiddleware(options: HealthMiddlewareOptions) {
|
||||
let lastSuccessTs = 0;
|
||||
|
||||
return async (
|
||||
_req: IncomingMessage,
|
||||
res: ServerResponse & {
|
||||
status: (code: number) => ServerResponse;
|
||||
json: (body: unknown) => void;
|
||||
},
|
||||
_req: unknown,
|
||||
res: HealthResponse,
|
||||
_next: () => void,
|
||||
): Promise<void> => {
|
||||
// Attempt to ping the upstream API
|
||||
|
||||
+37
-52
@@ -22,30 +22,48 @@ function createMockServer() {
|
||||
}
|
||||
|
||||
describe("registerGracefulShutdown", () => {
|
||||
let processOnSpy: ReturnType<typeof vi.spyOn>;
|
||||
let processExitSpy: ReturnType<typeof vi.spyOn>;
|
||||
/** Captured SIGTERM handlers from process.on */
|
||||
let capturedHandlers: Array<() => void>;
|
||||
let exitCalls: number[];
|
||||
|
||||
beforeEach(() => {
|
||||
processOnSpy = vi.spyOn(process, "on");
|
||||
processExitSpy = vi
|
||||
.spyOn(process, "exit")
|
||||
.mockImplementation(() => undefined as never);
|
||||
capturedHandlers = [];
|
||||
exitCalls = [];
|
||||
|
||||
// Intercept process.on("SIGTERM", handler) without fighting spy types
|
||||
process.on = ((event: string, listener: () => void) => {
|
||||
if (event === "SIGTERM") {
|
||||
capturedHandlers.push(listener);
|
||||
}
|
||||
return process;
|
||||
}) as typeof process.on;
|
||||
|
||||
// Intercept process.exit
|
||||
process.exit = ((code?: number) => {
|
||||
exitCalls.push(code ?? 0);
|
||||
}) as typeof process.exit;
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processOnSpy.mockRestore();
|
||||
processExitSpy.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function triggerSigterm(): void {
|
||||
expect(capturedHandlers.length).toBeGreaterThan(0);
|
||||
const handler = capturedHandlers[0];
|
||||
if (handler) handler();
|
||||
}
|
||||
|
||||
it("registers a SIGTERM handler", () => {
|
||||
const logger = createMockLogger();
|
||||
const server = createMockServer();
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
|
||||
expect(processOnSpy).toHaveBeenCalledWith("SIGTERM", expect.any(Function));
|
||||
expect(capturedHandlers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("calls server.close on SIGTERM", async () => {
|
||||
@@ -53,16 +71,8 @@ describe("registerGracefulShutdown", () => {
|
||||
const server = createMockServer();
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
triggerSigterm();
|
||||
|
||||
// Extract and call the SIGTERM handler
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
expect(sigtermCall).toBeDefined();
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
|
||||
// Let microtasks resolve
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(server.close).toHaveBeenCalled();
|
||||
@@ -73,12 +83,7 @@ describe("registerGracefulShutdown", () => {
|
||||
const server = createMockServer();
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
triggerSigterm();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
@@ -90,16 +95,11 @@ describe("registerGracefulShutdown", () => {
|
||||
const server = createMockServer();
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
triggerSigterm();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||
expect(exitCalls).toContain(0);
|
||||
});
|
||||
|
||||
it("force-exits with code 1 if drain times out", async () => {
|
||||
@@ -109,17 +109,12 @@ describe("registerGracefulShutdown", () => {
|
||||
};
|
||||
|
||||
registerGracefulShutdown({ logger, server, drainTimeoutMs: 1000 });
|
||||
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
triggerSigterm();
|
||||
|
||||
// Advance past the drain timeout
|
||||
await vi.advanceTimersByTimeAsync(1100);
|
||||
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(exitCalls).toContain(1);
|
||||
});
|
||||
|
||||
it("logs shutdown lifecycle events", async () => {
|
||||
@@ -127,12 +122,7 @@ describe("registerGracefulShutdown", () => {
|
||||
const server = createMockServer();
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
triggerSigterm();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
@@ -153,19 +143,14 @@ describe("registerGracefulShutdown", () => {
|
||||
};
|
||||
|
||||
registerGracefulShutdown({ logger, server });
|
||||
|
||||
const sigtermCall = processOnSpy.mock.calls.find(
|
||||
(call) => call[0] === "SIGTERM",
|
||||
);
|
||||
const handler = sigtermCall![1] as () => void;
|
||||
handler();
|
||||
triggerSigterm();
|
||||
|
||||
// Shouldn't have exited yet at 29s
|
||||
vi.advanceTimersByTime(29_000);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(exitCalls).toHaveLength(0);
|
||||
|
||||
// Should force-exit at 30s
|
||||
vi.advanceTimersByTime(2_000);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
expect(exitCalls).toContain(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user