fixed all tests
This commit is contained in:
parent
08f713d98b
commit
bd26ecf3bc
11 changed files with 457 additions and 794 deletions
|
|
@ -3,33 +3,30 @@ import { Shutdown } from '../src/shutdown';
|
|||
|
||||
describe('Shutdown Signal Handlers', () => {
|
||||
let shutdown: Shutdown;
|
||||
let processOnSpy: any;
|
||||
let processOnceSpy: any;
|
||||
let processExitSpy: any;
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
const originalOn = process.on;
|
||||
const originalOnce = process.once;
|
||||
const originalExit = process.exit;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset singleton instance
|
||||
(Shutdown as any).instance = null;
|
||||
|
||||
// Clean up global flag
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
|
||||
// Mock process.on
|
||||
// Mock process.once to capture signal handlers
|
||||
const listeners: Record<string, Function[]> = {};
|
||||
processOnSpy = mock((event: string, handler: Function) => {
|
||||
processOnceSpy = mock((event: string, handler: Function) => {
|
||||
if (!listeners[event]) {
|
||||
listeners[event] = [];
|
||||
}
|
||||
listeners[event].push(handler);
|
||||
return process;
|
||||
});
|
||||
process.on = processOnSpy as any;
|
||||
process.once = processOnceSpy as any;
|
||||
|
||||
// Mock process.exit
|
||||
processExitSpy = mock((code?: number) => {
|
||||
// Just record the call, don't throw
|
||||
return;
|
||||
// Just record the call, don't actually exit
|
||||
return undefined as never;
|
||||
});
|
||||
process.exit = processExitSpy as any;
|
||||
|
||||
|
|
@ -39,11 +36,8 @@ describe('Shutdown Signal Handlers', () => {
|
|||
|
||||
afterEach(() => {
|
||||
// Restore original methods
|
||||
process.on = originalOn;
|
||||
process.once = originalOnce;
|
||||
process.exit = originalExit;
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
(Shutdown as any).instance = null;
|
||||
|
|
@ -51,53 +45,29 @@ describe('Shutdown Signal Handlers', () => {
|
|||
});
|
||||
|
||||
describe('Signal Handler Registration', () => {
|
||||
it('should register Unix signal handlers on non-Windows', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
it('should register signal handlers on initialization with autoRegister', () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Check that Unix signals were registered
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should register Windows signal handlers on Windows', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Check that Windows signals were registered
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
expect(processOnSpy).not.toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
|
||||
// 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(processOnSpy).not.toHaveBeenCalled();
|
||||
expect(processOnceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not register handlers twice', () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
const callCount = processOnSpy.mock.calls.length;
|
||||
const callCount = processOnceSpy.mock.calls.length;
|
||||
|
||||
// Try to setup handlers again (internally)
|
||||
shutdown['setupSignalHandlers']();
|
||||
|
||||
// Should not register additional handlers
|
||||
expect(processOnSpy.mock.calls.length).toBe(callCount);
|
||||
expect(processOnceSpy.mock.calls.length).toBe(callCount);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -108,19 +78,19 @@ describe('Shutdown Signal Handlers', () => {
|
|||
shutdown.onShutdown(callback);
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
const sigtermHandler = listeners['SIGTERM']?.[0];
|
||||
|
||||
expect(sigtermHandler).toBeDefined();
|
||||
|
||||
// Trigger SIGTERM (this starts async shutdown)
|
||||
sigtermHandler();
|
||||
// Mock the shutdown method to track it was called
|
||||
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
||||
shutdown.shutdown = shutdownSpy;
|
||||
|
||||
// Verify flags are set immediately
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
|
||||
// Trigger SIGTERM
|
||||
await sigtermHandler();
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Now process.exit should have been called
|
||||
// Verify shutdown was called
|
||||
expect(shutdownSpy).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
|
|
@ -130,125 +100,120 @@ describe('Shutdown Signal Handlers', () => {
|
|||
shutdown.onShutdown(callback);
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigintHandler = listeners['SIGINT'][0];
|
||||
const sigintHandler = listeners['SIGINT']?.[0];
|
||||
|
||||
expect(sigintHandler).toBeDefined();
|
||||
|
||||
// Trigger SIGINT (this starts async shutdown)
|
||||
sigintHandler();
|
||||
// Mock the shutdown method
|
||||
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
||||
shutdown.shutdown = shutdownSpy;
|
||||
|
||||
// Verify flags are set immediately
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
// Trigger SIGINT
|
||||
await sigintHandler();
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Now process.exit should have been called
|
||||
// Verify shutdown was called
|
||||
expect(shutdownSpy).toHaveBeenCalled();
|
||||
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should handle uncaughtException', async () => {
|
||||
it('should exit with code 1 on shutdown error', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const exceptionHandler = listeners['uncaughtException'][0];
|
||||
|
||||
// Trigger uncaughtException (this starts async shutdown with exit code 1)
|
||||
exceptionHandler(new Error('Uncaught error'));
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Should exit with code 1 for uncaught exceptions
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle unhandledRejection', 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 rejectionHandler = listeners['unhandledRejection'][0];
|
||||
const sigtermHandler = listeners['SIGTERM']?.[0];
|
||||
|
||||
// Trigger unhandledRejection (this starts async shutdown with exit code 1)
|
||||
rejectionHandler(new Error('Unhandled rejection'));
|
||||
// Trigger SIGTERM
|
||||
await sigtermHandler();
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Should exit with code 1 for unhandled rejections
|
||||
// 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 });
|
||||
|
||||
// Start shutdown
|
||||
// Set shutting down flag
|
||||
shutdown['isShuttingDown'] = true;
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
const sigtermHandler = listeners['SIGTERM']?.[0];
|
||||
|
||||
// Mock shutdownAndExit to track calls
|
||||
const shutdownAndExitSpy = mock(() => Promise.resolve());
|
||||
shutdown.shutdownAndExit = shutdownAndExitSpy as any;
|
||||
// Mock shutdown to ensure it's not called
|
||||
const shutdownSpy = mock(shutdown.shutdown.bind(shutdown));
|
||||
shutdown.shutdown = shutdownSpy;
|
||||
|
||||
// Trigger SIGTERM
|
||||
sigtermHandler();
|
||||
await sigtermHandler();
|
||||
|
||||
// Should not call shutdownAndExit since already shutting down
|
||||
expect(shutdownAndExitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle shutdown failure in signal handler', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Mock shutdownAndExit to reject
|
||||
shutdown.shutdownAndExit = mock(async () => {
|
||||
throw new Error('Shutdown failed');
|
||||
}) as any;
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Trigger SIGTERM - should fall back to process.exit(1)
|
||||
sigtermHandler();
|
||||
|
||||
// Wait a bit for async shutdown to fail and fallback to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
// Should not call shutdown or exit
|
||||
expect(shutdownSpy).not.toHaveBeenCalled();
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Flag Behavior', () => {
|
||||
it('should set global shutdown flag on signal', async () => {
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
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;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Trigger signal (this sets the flag immediately)
|
||||
sigtermHandler();
|
||||
|
||||
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
|
||||
|
||||
// Wait for async shutdown to complete to avoid hanging promises
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// 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 check global flag in isShutdownSignalReceived', () => {
|
||||
shutdown = new Shutdown({ autoRegister: false });
|
||||
it('should work with default options', () => {
|
||||
// Create with defaults
|
||||
shutdown = new Shutdown();
|
||||
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(false);
|
||||
|
||||
// Set global flag
|
||||
(global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true;
|
||||
|
||||
// Even without instance flag, should return true
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
|
||||
// Clean up
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
// Should auto-register by default
|
||||
expect(processOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue