import { EventEmitter } from 'node:events' import { afterEach, describe, expect, it, vi } from 'vitest' import { createDb, closeDb, migrateSchema } from '@authportal/db' import { registerShutdown } from '../src/lib/shutdown.js' import { buildTestApp } from './helpers.js' function fakeApp(close: () => Promise) { return { close, log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, } as unknown as Parameters[0] } describe('registerShutdown', () => { afterEach(() => { vi.useRealTimers() }) it('закрывает приложение один раз и выходит с 0', async () => { const close = vi.fn(async () => {}) const exit = vi.fn() const proc = new EventEmitter() registerShutdown(fakeApp(close), { timeoutMs: 1000, exit, proc: proc as never, }) proc.emit('SIGTERM') await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0)) expect(close).toHaveBeenCalledTimes(1) }) it('повторный сигнал форсирует выход, close не вызывается дважды', async () => { let release!: () => void const close = vi.fn( () => new Promise((r) => (release = r)), ) const exit = vi.fn() const proc = new EventEmitter() registerShutdown(fakeApp(close), { timeoutMs: 1000, exit, proc: proc as never, }) proc.emit('SIGTERM') proc.emit('SIGINT') expect(exit).toHaveBeenCalledWith(1) expect(close).toHaveBeenCalledTimes(1) release() }) it('по таймауту принудительно выходит с кодом 1', async () => { vi.useFakeTimers() const exit = vi.fn() const proc = new EventEmitter() registerShutdown( fakeApp(() => new Promise(() => {})), { timeoutMs: 500, exit, proc: proc as never }, ) proc.emit('SIGTERM') await vi.advanceTimersByTimeAsync(501) expect(exit).toHaveBeenCalledWith(1) }) it('ошибка close → выход с кодом 1', async () => { const exit = vi.fn() const proc = new EventEmitter() registerShutdown( fakeApp(async () => { throw new Error('boom') }), { timeoutMs: 1000, exit, proc: proc as never }, ) proc.emit('SIGTERM') await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1)) }) it('unhandledRejection запускает shutdown с кодом 1', async () => { const exit = vi.fn() const proc = new EventEmitter() registerShutdown(fakeApp(async () => {}), { timeoutMs: 1000, exit, proc: proc as never, }) proc.emit('unhandledRejection', new Error('x')) await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1)) }) it('dispose снимает обработчики', () => { const proc = new EventEmitter() const h = registerShutdown(fakeApp(async () => {}), { timeoutMs: 1000, exit: vi.fn(), proc: proc as never, }) h.dispose() expect(proc.listenerCount('SIGTERM')).toBe(0) expect(proc.listenerCount('unhandledRejection')).toBe(0) }) }) describe('closeDb', () => { it('идемпотентно закрывает БД', () => { const { sqlite } = createDb('sqlite::memory:') migrateSchema(sqlite) closeDb(sqlite) expect(sqlite.open).toBe(false) expect(() => closeDb(sqlite)).not.toThrow() expect(() => sqlite.prepare('SELECT 1')).toThrow() }) it('app.close() закрывает БД приложения', async () => { const app = await buildTestApp() await app.ready() expect(app.sqlite.open).toBe(true) await app.close() expect(app.sqlite.open).toBe(false) }) })