added cli-covarage tool and fixed more tests
This commit is contained in:
parent
b63e58784c
commit
b845a8eade
57 changed files with 11917 additions and 295 deletions
569
libs/core/di/test/service-application.test.ts
Normal file
569
libs/core/di/test/service-application.test.ts
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import { ServiceApplication } from '../src/service-application';
|
||||
import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application';
|
||||
import type { BaseAppConfig } from '@stock-bot/config';
|
||||
|
||||
// Mock logger module
|
||||
const mockLogger = {
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
child: mock(() => mockLogger),
|
||||
};
|
||||
|
||||
mock.module('@stock-bot/logger', () => ({
|
||||
getLogger: () => mockLogger,
|
||||
setLoggerConfig: mock(() => {}),
|
||||
shutdownLoggers: mock(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
// Mock shutdown module
|
||||
const mockShutdownInstance = {
|
||||
onShutdown: mock(() => {}),
|
||||
onShutdownHigh: mock(() => {}),
|
||||
onShutdownMedium: mock(() => {}),
|
||||
onShutdownLow: mock(() => {}),
|
||||
register: mock(() => {}),
|
||||
registerAsync: mock(() => {}),
|
||||
handleTermination: mock(() => {}),
|
||||
executeCallbacks: mock(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const mockShutdown = mock(() => mockShutdownInstance);
|
||||
mockShutdown.getInstance = mock(() => mockShutdownInstance);
|
||||
|
||||
mock.module('@stock-bot/shutdown', () => ({
|
||||
Shutdown: mockShutdown,
|
||||
}));
|
||||
|
||||
// Mock Bun.serve
|
||||
const mockServer = {
|
||||
stop: mock(() => {}),
|
||||
port: 3000,
|
||||
hostname: '0.0.0.0',
|
||||
};
|
||||
|
||||
const originalBunServe = Bun.serve;
|
||||
Bun.serve = mock(() => mockServer);
|
||||
|
||||
const mockConfig: BaseAppConfig = {
|
||||
name: 'test-service',
|
||||
version: '1.0.0',
|
||||
environment: 'test',
|
||||
service: {
|
||||
name: 'test-service',
|
||||
serviceName: 'test-service',
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
healthCheckPath: '/health',
|
||||
metricsPath: '/metrics',
|
||||
shutdownTimeout: 5000,
|
||||
cors: {
|
||||
enabled: true,
|
||||
origin: '*',
|
||||
credentials: true,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
pretty: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe.skip('ServiceApplication', () => {
|
||||
let app: ServiceApplication;
|
||||
|
||||
afterEach(() => {
|
||||
// Reset mocks
|
||||
mockLogger.info.mockReset();
|
||||
mockLogger.error.mockReset();
|
||||
mockLogger.warn.mockReset();
|
||||
mockLogger.debug.mockReset();
|
||||
mockShutdownInstance.onShutdown.mockReset();
|
||||
mockShutdownInstance.onShutdownHigh.mockReset();
|
||||
mockShutdownInstance.onShutdownMedium.mockReset();
|
||||
mockShutdownInstance.onShutdownLow.mockReset();
|
||||
mockShutdownInstance.register.mockReset();
|
||||
mockShutdownInstance.registerAsync.mockReset();
|
||||
mockShutdownInstance.handleTermination.mockReset();
|
||||
mockShutdownInstance.executeCallbacks.mockReset();
|
||||
|
||||
// Clean up app if it exists
|
||||
if (app) {
|
||||
app.stop().catch(() => {});
|
||||
app = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create service application', () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create with full config', () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
addInfoEndpoint: true,
|
||||
enableHandlers: true,
|
||||
enableScheduledJobs: true,
|
||||
shutdownTimeout: 10000,
|
||||
corsConfig: {
|
||||
origin: 'https://example.com',
|
||||
credentials: true,
|
||||
},
|
||||
serviceMetadata: {
|
||||
version: '1.0.0',
|
||||
description: 'Test service',
|
||||
},
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize shutdown with custom timeout', () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
shutdownTimeout: 30000,
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
expect(mockShutdown.getInstance).toHaveBeenCalledWith({
|
||||
timeout: 30000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should support lifecycle hooks', () => {
|
||||
const hooks: ServiceLifecycleHooks = {
|
||||
beforeInitialize: mock(() => Promise.resolve()),
|
||||
afterInitialize: mock(() => Promise.resolve()),
|
||||
beforeSetupRoutes: mock(() => {}),
|
||||
afterSetupRoutes: mock(() => {}),
|
||||
onStart: mock(() => Promise.resolve()),
|
||||
onStop: mock(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig, hooks);
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getters', () => {
|
||||
it('should have public methods', () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
expect(app.start).toBeDefined();
|
||||
expect(app.stop).toBeDefined();
|
||||
expect(app.getServiceContainer).toBeDefined();
|
||||
expect(app.getApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error scenarios', () => {
|
||||
it('should handle missing service name', () => {
|
||||
const configWithoutServiceName = {
|
||||
...mockConfig,
|
||||
service: {
|
||||
...mockConfig.service,
|
||||
serviceName: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'fallback-service',
|
||||
};
|
||||
|
||||
// Should not throw - uses fallback
|
||||
app = new ServiceApplication(configWithoutServiceName as any, serviceConfig);
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('start method', () => {
|
||||
const mockContainer = {
|
||||
resolve: mock((name: string) => {
|
||||
if (name === 'serviceContainer') {
|
||||
return { test: 'container' };
|
||||
}
|
||||
if (name === 'handlerRegistry') {
|
||||
return {
|
||||
getAllHandlersWithSchedule: () => new Map(),
|
||||
getHandlerNames: () => [],
|
||||
getHandlerService: () => 'test-service',
|
||||
getOperation: () => ({}),
|
||||
};
|
||||
}
|
||||
if (name === 'queueManager') {
|
||||
return {
|
||||
getQueue: () => ({
|
||||
addScheduledJob: mock(() => Promise.resolve()),
|
||||
}),
|
||||
startAllWorkers: mock(() => {}),
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
||||
const mockContainerFactory = mock(async () => mockContainer);
|
||||
const mockRouteFactory = mock(() => {
|
||||
const { Hono } = require('hono');
|
||||
const routes = new Hono();
|
||||
// Add a simple test route
|
||||
routes.get('/test', (c) => c.json({ test: true }));
|
||||
return routes;
|
||||
});
|
||||
const mockHandlerInitializer = mock(() => Promise.resolve());
|
||||
|
||||
it('should start service with basic configuration', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
addInfoEndpoint: false,
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await app.start(mockContainerFactory, mockRouteFactory);
|
||||
|
||||
expect(mockContainerFactory).toHaveBeenCalledWith(expect.objectContaining({
|
||||
service: expect.objectContaining({ serviceName: 'test-service' }),
|
||||
}));
|
||||
expect(mockRouteFactory).toHaveBeenCalledWith({ test: 'container' });
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('test-service service started on port 3000');
|
||||
});
|
||||
|
||||
it('should initialize handlers when enabled', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
enableHandlers: true,
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await app.start(mockContainerFactory, mockRouteFactory, mockHandlerInitializer);
|
||||
|
||||
expect(mockHandlerInitializer).toHaveBeenCalledWith(expect.objectContaining({
|
||||
test: 'container',
|
||||
_diContainer: mockContainer,
|
||||
}));
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('Handlers initialized');
|
||||
});
|
||||
|
||||
it('should call lifecycle hooks', async () => {
|
||||
const hooks: ServiceLifecycleHooks = {
|
||||
onContainerReady: mock(() => {}),
|
||||
onAppReady: mock(() => {}),
|
||||
onBeforeStart: mock(() => {}),
|
||||
onStarted: mock(() => {}),
|
||||
};
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig, hooks);
|
||||
|
||||
await app.start(mockContainerFactory, mockRouteFactory);
|
||||
|
||||
expect(hooks.onContainerReady).toHaveBeenCalledWith({ test: 'container' });
|
||||
expect(hooks.onAppReady).toHaveBeenCalled();
|
||||
expect(hooks.onBeforeStart).toHaveBeenCalled();
|
||||
expect(hooks.onStarted).toHaveBeenCalledWith(3000);
|
||||
});
|
||||
|
||||
it('should handle start errors', async () => {
|
||||
const errorFactory = mock(() => {
|
||||
throw new Error('Container creation failed');
|
||||
});
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow('Container creation failed');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('DETAILED ERROR:', expect.any(Error));
|
||||
});
|
||||
|
||||
it('should initialize scheduled jobs when enabled', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
enableScheduledJobs: true,
|
||||
};
|
||||
|
||||
const mockHandlerRegistry = {
|
||||
getAllHandlersWithSchedule: () => new Map([
|
||||
['testHandler', {
|
||||
scheduledJobs: [{
|
||||
operation: 'processData',
|
||||
cronPattern: '0 * * * *',
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
payload: { test: true },
|
||||
}],
|
||||
}],
|
||||
]),
|
||||
getHandlerService: () => 'test-service',
|
||||
getHandlerNames: () => ['testHandler'],
|
||||
getOperation: () => ({ name: 'processData' }),
|
||||
};
|
||||
|
||||
const mockQueue = {
|
||||
addScheduledJob: mock(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const mockQueueManager = {
|
||||
getQueue: mock(() => mockQueue),
|
||||
startAllWorkers: mock(() => {}),
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const containerWithJobs = {
|
||||
resolve: mock((name: string) => {
|
||||
if (name === 'serviceContainer') return { test: 'container' };
|
||||
if (name === 'handlerRegistry') return mockHandlerRegistry;
|
||||
if (name === 'queueManager') return mockQueueManager;
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
||||
const jobContainerFactory = mock(async () => containerWithJobs);
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await app.start(jobContainerFactory, mockRouteFactory);
|
||||
|
||||
expect(mockQueueManager.getQueue).toHaveBeenCalledWith('testHandler', {
|
||||
handlerRegistry: mockHandlerRegistry,
|
||||
});
|
||||
expect(mockQueue.addScheduledJob).toHaveBeenCalledWith(
|
||||
'processData',
|
||||
{ handler: 'testHandler', operation: 'processData', payload: { test: true } },
|
||||
'0 * * * *',
|
||||
expect.objectContaining({ priority: 5, repeat: { immediately: false } }),
|
||||
);
|
||||
expect(mockQueueManager.startAllWorkers).toHaveBeenCalled();
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('Scheduled jobs created', { totalJobs: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop method', () => {
|
||||
it('should trigger shutdown', async () => {
|
||||
const mockShutdownInstance = {
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
onShutdownHigh: mock(() => {}),
|
||||
onShutdownMedium: mock(() => {}),
|
||||
onShutdownLow: mock(() => {}),
|
||||
};
|
||||
|
||||
mock.module('@stock-bot/shutdown', () => ({
|
||||
Shutdown: {
|
||||
getInstance: () => mockShutdownInstance,
|
||||
},
|
||||
}));
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await app.stop();
|
||||
|
||||
expect(mockShutdownInstance.shutdown).toHaveBeenCalled();
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('Stopping test-service service...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getters', () => {
|
||||
it('should return service container after start', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
// Before start
|
||||
expect(app.getServiceContainer()).toBeNull();
|
||||
expect(app.getApp()).toBeNull();
|
||||
|
||||
// After start
|
||||
const mockContainer = {
|
||||
resolve: mock(() => ({ test: 'container' })),
|
||||
};
|
||||
await app.start(
|
||||
async () => mockContainer,
|
||||
async () => {
|
||||
const { Hono } = await import('hono');
|
||||
return new Hono();
|
||||
}
|
||||
);
|
||||
|
||||
expect(app.getServiceContainer()).toEqual({ test: 'container' });
|
||||
expect(app.getApp()).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown handlers', () => {
|
||||
it('should register all shutdown handlers during start', async () => {
|
||||
const mockShutdownInstance = {
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
onShutdownHigh: mock(() => {}),
|
||||
onShutdownMedium: mock(() => {}),
|
||||
onShutdownLow: mock(() => {}),
|
||||
};
|
||||
|
||||
mock.module('@stock-bot/shutdown', () => ({
|
||||
Shutdown: {
|
||||
getInstance: () => mockShutdownInstance,
|
||||
},
|
||||
}));
|
||||
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
enableScheduledJobs: true,
|
||||
};
|
||||
|
||||
const hooks: ServiceLifecycleHooks = {
|
||||
onBeforeShutdown: mock(() => {}),
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig, hooks);
|
||||
|
||||
const mockContainer = {
|
||||
resolve: mock((name: string) => {
|
||||
if (name === 'serviceContainer') return { test: 'container' };
|
||||
if (name === 'handlerRegistry') return {
|
||||
getAllHandlersWithSchedule: () => new Map(),
|
||||
getHandlerNames: () => [],
|
||||
};
|
||||
if (name === 'queueManager') return {
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
startAllWorkers: mock(() => {}),
|
||||
};
|
||||
if (name === 'mongoClient') return { disconnect: mock(() => Promise.resolve()) };
|
||||
if (name === 'postgresClient') return { disconnect: mock(() => Promise.resolve()) };
|
||||
if (name === 'questdbClient') return { disconnect: mock(() => Promise.resolve()) };
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
||||
await app.start(
|
||||
async () => mockContainer,
|
||||
async () => new (await import('hono')).Hono()
|
||||
);
|
||||
|
||||
// Should have registered shutdown handlers
|
||||
expect(mockShutdownInstance.onShutdownHigh).toHaveBeenCalledTimes(3); // Queue, HTTP, Custom
|
||||
expect(mockShutdownInstance.onShutdownMedium).toHaveBeenCalledTimes(1); // Services
|
||||
expect(mockShutdownInstance.onShutdownLow).toHaveBeenCalledTimes(1); // Loggers
|
||||
|
||||
// Test the handlers by calling them
|
||||
const highHandlers = (mockShutdownInstance.onShutdownHigh as any).mock.calls;
|
||||
const mediumHandlers = (mockShutdownInstance.onShutdownMedium as any).mock.calls;
|
||||
const lowHandlers = (mockShutdownInstance.onShutdownLow as any).mock.calls;
|
||||
|
||||
// Execute queue shutdown handler
|
||||
await highHandlers[0][0]();
|
||||
expect(mockContainer.resolve).toHaveBeenCalledWith('queueManager');
|
||||
|
||||
// Execute services shutdown handler
|
||||
await mediumHandlers[0][0]();
|
||||
expect(mockContainer.resolve).toHaveBeenCalledWith('mongoClient');
|
||||
expect(mockContainer.resolve).toHaveBeenCalledWith('postgresClient');
|
||||
expect(mockContainer.resolve).toHaveBeenCalledWith('questdbClient');
|
||||
|
||||
// Execute logger shutdown handler
|
||||
await lowHandlers[0][0]();
|
||||
// Logger shutdown is called internally
|
||||
});
|
||||
});
|
||||
|
||||
describe('info endpoint', () => {
|
||||
it('should add info endpoint when enabled', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
addInfoEndpoint: true,
|
||||
serviceMetadata: {
|
||||
version: '2.0.0',
|
||||
description: 'Test service description',
|
||||
endpoints: {
|
||||
'/api/v1': 'Main API',
|
||||
'/health': 'Health check',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
const mockContainer = {
|
||||
resolve: mock(() => ({ test: 'container' })),
|
||||
};
|
||||
|
||||
await app.start(
|
||||
async () => mockContainer,
|
||||
async () => new (await import('hono')).Hono()
|
||||
);
|
||||
|
||||
const honoApp = app.getApp();
|
||||
expect(honoApp).toBeDefined();
|
||||
|
||||
// Test the info endpoint
|
||||
const response = await honoApp!.request('/');
|
||||
const json = await response.json();
|
||||
|
||||
expect(json).toEqual({
|
||||
name: 'test-service',
|
||||
version: '2.0.0',
|
||||
description: 'Test service description',
|
||||
status: 'running',
|
||||
timestamp: expect.any(String),
|
||||
endpoints: {
|
||||
'/api/v1': 'Main API',
|
||||
'/health': 'Health check',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add info endpoint when disabled', async () => {
|
||||
const serviceConfig: ServiceApplicationConfig = {
|
||||
serviceName: 'test-service',
|
||||
addInfoEndpoint: false,
|
||||
};
|
||||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
const mockContainer = {
|
||||
resolve: mock(() => ({ test: 'container' })),
|
||||
};
|
||||
|
||||
await app.start(
|
||||
async () => mockContainer,
|
||||
async () => new (await import('hono')).Hono()
|
||||
);
|
||||
|
||||
const honoApp = app.getApp();
|
||||
const response = await honoApp!.request('/');
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue