n8n/packages/cli/test/unit/shutdown/Shutdown.service.test.ts
Tomi Turtiainen 3a881be6c2
feat(core): Unify application components shutdown (#8097)
## Summary

Add `ShutdownService` and `OnShutdown` decorator for more unified way to
shutdown different components. Use this new way in the following
components:

- HTTP(S) server
- Pruning service
- Push connection
- License

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
2023-12-22 12:39:58 +02:00

128 lines
4 KiB
TypeScript

import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import type { ServiceClass } from '@/shutdown/Shutdown.service';
import { ShutdownService } from '@/shutdown/Shutdown.service';
import Container from 'typedi';
class MockComponent {
onShutdown() {}
}
describe('ShutdownService', () => {
let shutdownService: ShutdownService;
let mockComponent: MockComponent;
let onShutdownSpy: jest.SpyInstance;
let mockErrorReporterProxy: jest.SpyInstance;
beforeEach(() => {
shutdownService = new ShutdownService(mock());
mockComponent = new MockComponent();
Container.set(MockComponent, mockComponent);
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
mockErrorReporterProxy = jest.spyOn(ErrorReporterProxy, 'error').mockImplementation(() => {});
});
describe('shutdown', () => {
it('should signal shutdown', () => {
shutdownService.register(10, {
serviceClass: MockComponent as unknown as ServiceClass,
methodName: 'onShutdown',
});
shutdownService.shutdown();
expect(onShutdownSpy).toBeCalledTimes(1);
});
it('should signal shutdown in the priority order', async () => {
class MockService {
onShutdownHighPrio() {}
onShutdownLowPrio() {}
}
const order: string[] = [];
const mockService = new MockService();
Container.set(MockService, mockService);
jest.spyOn(mockService, 'onShutdownHighPrio').mockImplementation(() => order.push('high'));
jest.spyOn(mockService, 'onShutdownLowPrio').mockImplementation(() => order.push('low'));
shutdownService.register(100, {
serviceClass: MockService as unknown as ServiceClass,
methodName: 'onShutdownHighPrio',
});
shutdownService.register(10, {
serviceClass: MockService as unknown as ServiceClass,
methodName: 'onShutdownLowPrio',
});
shutdownService.shutdown();
await shutdownService.waitForShutdown();
expect(order).toEqual(['high', 'low']);
});
it('should throw error if shutdown is already in progress', () => {
shutdownService.register(10, {
methodName: 'onShutdown',
serviceClass: MockComponent as unknown as ServiceClass,
});
shutdownService.shutdown();
expect(() => shutdownService.shutdown()).toThrow('App is already shutting down');
});
it('should report error if component shutdown fails', async () => {
const componentError = new Error('Something went wrong');
onShutdownSpy.mockImplementation(() => {
throw componentError;
});
shutdownService.register(10, {
serviceClass: MockComponent as unknown as ServiceClass,
methodName: 'onShutdown',
});
shutdownService.shutdown();
await shutdownService.waitForShutdown();
expect(mockErrorReporterProxy).toHaveBeenCalledTimes(1);
const error = mockErrorReporterProxy.mock.calls[0][0];
expect(error).toBeInstanceOf(ApplicationError);
expect(error.message).toBe('Failed to shutdown gracefully');
expect(error.extra).toEqual({
component: 'MockComponent.onShutdown()',
});
expect(error.cause).toBe(componentError);
});
});
describe('waitForShutdown', () => {
it('should wait for shutdown', async () => {
shutdownService.register(10, {
serviceClass: MockComponent as unknown as ServiceClass,
methodName: 'onShutdown',
});
shutdownService.shutdown();
await expect(shutdownService.waitForShutdown()).resolves.toBeUndefined();
});
it('should throw error if app is not shutting down', async () => {
await expect(async () => shutdownService.waitForShutdown()).rejects.toThrow(
'App is not shutting down',
);
});
});
describe('isShuttingDown', () => {
it('should return true if app is shutting down', () => {
shutdownService.register(10, {
serviceClass: MockComponent as unknown as ServiceClass,
methodName: 'onShutdown',
});
shutdownService.shutdown();
expect(shutdownService.isShuttingDown()).toBe(true);
});
it('should return false if app is not shutting down', () => {
expect(shutdownService.isShuttingDown()).toBe(false);
});
});
});