219 lines
No EOL
6.8 KiB
TypeScript
219 lines
No EOL
6.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
import { Shutdown } from '../src/shutdown';
|
|
|
|
describe('Shutdown Signal Handlers', () => {
|
|
let shutdown: Shutdown;
|
|
let processOnceSpy: any;
|
|
let processExitSpy: any;
|
|
const originalOnce = process.once;
|
|
const originalExit = process.exit;
|
|
|
|
beforeEach(() => {
|
|
// Reset singleton instance
|
|
(Shutdown as any).instance = null;
|
|
|
|
// Mock process.once to capture signal handlers
|
|
const listeners: Record<string, Function[]> = {};
|
|
processOnceSpy = mock((event: string, handler: Function) => {
|
|
if (!listeners[event]) {
|
|
listeners[event] = [];
|
|
}
|
|
listeners[event].push(handler);
|
|
return process;
|
|
});
|
|
process.once = processOnceSpy as any;
|
|
|
|
// Mock process.exit
|
|
processExitSpy = mock((code?: number) => {
|
|
// Just record the call, don't actually exit
|
|
return undefined as never;
|
|
});
|
|
process.exit = processExitSpy as any;
|
|
|
|
// Store listeners for manual triggering
|
|
(global as any).__testListeners = listeners;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original methods
|
|
process.once = originalOnce;
|
|
process.exit = originalExit;
|
|
|
|
// Clean up
|
|
(Shutdown as any).instance = null;
|
|
delete (global as any).__testListeners;
|
|
});
|
|
|
|
describe('Signal Handler Registration', () => {
|
|
it('should register signal handlers on initialization with autoRegister', () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
|
|
// Check that signals were registered
|
|
expect(processOnceSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
|
expect(processOnceSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
});
|
|
|
|
it('should not register handlers when autoRegister is false', () => {
|
|
shutdown = new Shutdown({ autoRegister: false });
|
|
|
|
expect(processOnceSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not register handlers twice', () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
const callCount = processOnceSpy.mock.calls.length;
|
|
|
|
// Try to setup handlers again (internally)
|
|
shutdown['setupSignalHandlers']();
|
|
|
|
// Should not register additional handlers
|
|
expect(processOnceSpy.mock.calls.length).toBe(callCount);
|
|
});
|
|
});
|
|
|
|
describe('Signal Handler Behavior', () => {
|
|
it('should handle SIGTERM signal', async () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
const callback = mock(async () => {});
|
|
shutdown.onShutdown(callback);
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
const sigtermHandler = listeners['SIGTERM']?.[0];
|
|
|
|
expect(sigtermHandler).toBeDefined();
|
|
|
|
// Mock the shutdown method to track it was called
|
|
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
|
shutdown.shutdown = shutdownSpy;
|
|
|
|
// Trigger SIGTERM
|
|
await sigtermHandler();
|
|
|
|
// Verify shutdown was called
|
|
expect(shutdownSpy).toHaveBeenCalled();
|
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it('should handle SIGINT signal', async () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
const callback = mock(async () => {});
|
|
shutdown.onShutdown(callback);
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
const sigintHandler = listeners['SIGINT']?.[0];
|
|
|
|
expect(sigintHandler).toBeDefined();
|
|
|
|
// Mock the shutdown method
|
|
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
|
shutdown.shutdown = shutdownSpy;
|
|
|
|
// Trigger SIGINT
|
|
await sigintHandler();
|
|
|
|
// Verify shutdown was called
|
|
expect(shutdownSpy).toHaveBeenCalled();
|
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it('should exit with code 1 on shutdown error', async () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
|
|
// Make shutdown throw an error
|
|
shutdown.shutdown = mock(async () => {
|
|
throw new Error('Shutdown failed');
|
|
});
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
const sigtermHandler = listeners['SIGTERM']?.[0];
|
|
|
|
// Trigger SIGTERM
|
|
await sigtermHandler();
|
|
|
|
// Should exit with code 1 on error
|
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it('should not process signal if already shutting down', async () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
|
|
// Set shutting down flag
|
|
shutdown['isShuttingDown'] = true;
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
const sigtermHandler = listeners['SIGTERM']?.[0];
|
|
|
|
// Mock shutdown to ensure it's not called
|
|
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
|
shutdown.shutdown = shutdownSpy;
|
|
|
|
// Trigger SIGTERM
|
|
await sigtermHandler();
|
|
|
|
// Should not call shutdown or exit
|
|
expect(shutdownSpy).not.toHaveBeenCalled();
|
|
expect(processExitSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Singleton Behavior with Signals', () => {
|
|
it('should use singleton instance for signal handling', () => {
|
|
const instance1 = Shutdown.getInstance();
|
|
const instance2 = Shutdown.getInstance();
|
|
|
|
expect(instance1).toBe(instance2);
|
|
|
|
// Only one set of signal handlers should be registered
|
|
expect(processOnceSpy).toHaveBeenCalledTimes(2); // SIGTERM and SIGINT
|
|
});
|
|
|
|
it('should handle callbacks registered before signal', async () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
|
|
const callback1 = mock(async () => {});
|
|
const callback2 = mock(async () => {});
|
|
|
|
shutdown.onShutdown(callback1, 10);
|
|
shutdown.onShutdown(callback2, 20);
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
const sigtermHandler = listeners['SIGTERM']?.[0];
|
|
|
|
// Replace shutdown method to test callback execution
|
|
const originalShutdown = shutdown.shutdown.bind(shutdown);
|
|
let shutdownCalled = false;
|
|
shutdown.shutdown = mock(async () => {
|
|
shutdownCalled = true;
|
|
await originalShutdown();
|
|
});
|
|
|
|
// Trigger signal
|
|
await sigtermHandler();
|
|
|
|
expect(shutdownCalled).toBe(true);
|
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle missing signal handler gracefully', () => {
|
|
shutdown = new Shutdown({ autoRegister: true });
|
|
|
|
const listeners = (global as any).__testListeners;
|
|
|
|
// Verify handlers exist
|
|
expect(listeners['SIGTERM']).toBeDefined();
|
|
expect(listeners['SIGINT']).toBeDefined();
|
|
expect(listeners['SIGTERM'].length).toBeGreaterThan(0);
|
|
expect(listeners['SIGINT'].length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should work with default options', () => {
|
|
// Create with defaults
|
|
shutdown = new Shutdown();
|
|
|
|
// Should auto-register by default
|
|
expect(processOnceSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
}); |