Files
flights_web/src/server/shutdown.test.ts
T
gnezim 7db39cbeec Fix type errors and lint warnings in health and shutdown modules
Use proper type-safe interfaces instead of Node.js http types for the
health handler, and avoid vi.spyOn type issues in shutdown tests by
directly intercepting process.on.
2026-04-15 00:59:59 +03:00

157 lines
4.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { registerGracefulShutdown } from "./shutdown.js";
import type { Logger } from "@/observability/logger/types.js";
function createMockLogger(): Logger & { flush: ReturnType<typeof vi.fn> } {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
child: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
};
logger.child.mockReturnValue(logger);
return logger as Logger & { flush: ReturnType<typeof vi.fn> };
}
function createMockServer() {
return {
close: vi.fn((cb: () => void) => cb()),
};
}
describe("registerGracefulShutdown", () => {
/** Captured SIGTERM handlers from process.on */
let capturedHandlers: Array<() => void>;
let exitCalls: number[];
beforeEach(() => {
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(() => {
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(capturedHandlers).toHaveLength(1);
});
it("calls server.close on SIGTERM", async () => {
const logger = createMockLogger();
const server = createMockServer();
registerGracefulShutdown({ logger, server });
triggerSigterm();
await vi.advanceTimersByTimeAsync(0);
expect(server.close).toHaveBeenCalled();
});
it("flushes logger after server closes", async () => {
const logger = createMockLogger();
const server = createMockServer();
registerGracefulShutdown({ logger, server });
triggerSigterm();
await vi.advanceTimersByTimeAsync(0);
expect(logger.flush).toHaveBeenCalled();
});
it("exits with code 0 after successful drain", async () => {
const logger = createMockLogger();
const server = createMockServer();
registerGracefulShutdown({ logger, server });
triggerSigterm();
await vi.advanceTimersByTimeAsync(0);
expect(exitCalls).toContain(0);
});
it("force-exits with code 1 if drain times out", async () => {
const logger = createMockLogger();
const server = {
close: vi.fn(), // never calls callback — simulates stuck connections
};
registerGracefulShutdown({ logger, server, drainTimeoutMs: 1000 });
triggerSigterm();
// Advance past the drain timeout
await vi.advanceTimersByTimeAsync(1100);
expect(exitCalls).toContain(1);
});
it("logs shutdown lifecycle events", async () => {
const logger = createMockLogger();
const server = createMockServer();
registerGracefulShutdown({ logger, server });
triggerSigterm();
await vi.advanceTimersByTimeAsync(0);
expect(logger.info).toHaveBeenCalledWith(
"SIGTERM received, starting graceful shutdown",
expect.any(Object),
);
expect(logger.info).toHaveBeenCalledWith(
"Server closed, flushing logs",
undefined,
);
});
it("uses default drainTimeoutMs of 30000", () => {
const logger = createMockLogger();
const server = {
close: vi.fn(), // never calls callback
};
registerGracefulShutdown({ logger, server });
triggerSigterm();
// Shouldn't have exited yet at 29s
vi.advanceTimersByTime(29_000);
expect(exitCalls).toHaveLength(0);
// Should force-exit at 30s
vi.advanceTimersByTime(2_000);
expect(exitCalls).toContain(1);
});
});