stock-bot/libs/core/di/test/registration.test.ts

415 lines
11 KiB
TypeScript

import { asClass, asFunction, asValue, createContainer } from 'awilix';
import { describe, expect, it, mock } from 'bun:test';
import {
registerApplicationServices,
registerCacheServices,
registerDatabaseServices,
} from '../src/registrations';
// Mock the queue module
mock.module('@stock-bot/queue', () => ({
createServiceCache: mock(() => ({
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve()),
del: mock(() => Promise.resolve()),
})),
}));
describe('DI Registrations', () => {
describe('registerCacheServices', () => {
it('should register null cache when redis disabled', () => {
const container = createContainer();
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
redis: {
enabled: false,
host: 'localhost',
port: 6379,
},
} as any;
registerCacheServices(container, config);
const cache = container.resolve('cache');
expect(cache).toBeNull();
});
it('should register redis cache when redis config exists', () => {
const container = createContainer();
// Register logger first as it's a dependency
container.register({
logger: asValue({
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
}),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
redis: {
enabled: true,
host: 'localhost',
port: 6379,
db: 1,
},
} as any;
registerCacheServices(container, config);
const cache = container.resolve('cache');
expect(cache).toBeDefined();
});
it('should register both cache and globalCache', () => {
const container = createContainer();
// Register logger dependency
container.register({
logger: asValue({
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
}),
});
const config = {
service: {
name: 'test-service',
serviceName: 'test-service',
},
redis: {
enabled: true,
host: 'localhost',
port: 6379,
db: 1,
},
} as any;
registerCacheServices(container, config);
const cache = container.resolve('cache');
const globalCache = container.resolve('globalCache');
expect(cache).toBeDefined();
expect(globalCache).toBeDefined();
});
});
describe('registerDatabaseServices', () => {
it('should register MongoDB when config exists', () => {
const container = createContainer();
// Mock MongoDB client
const mockMongoClient = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
getDb: mock(() => ({})),
};
// Mock the MongoDB factory
mock.module('@stock-bot/mongodb', () => ({
MongoDBClient: class {
constructor() {
return mockMongoClient;
}
},
}));
const config = {
mongodb: {
enabled: true,
uri: 'mongodb://localhost',
database: 'test-db',
},
} as any;
registerDatabaseServices(container, config);
expect(container.hasRegistration('mongoClient')).toBe(true);
});
it('should register PostgreSQL when config exists', () => {
const container = createContainer();
// Mock Postgres client
const mockPostgresClient = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
query: mock(() => Promise.resolve({ rows: [] })),
};
// Mock the Postgres factory
mock.module('@stock-bot/postgres', () => ({
PostgresClient: class {
constructor() {
return mockPostgresClient;
}
},
}));
const config = {
postgres: {
enabled: true,
host: 'localhost',
port: 5432,
user: 'user',
password: 'pass',
database: 'test-db',
},
} as any;
registerDatabaseServices(container, config);
expect(container.hasRegistration('postgresClient')).toBe(true);
});
it('should register QuestDB when config exists', () => {
const container = createContainer();
// Mock QuestDB client
const mockQuestdbClient = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
query: mock(() => Promise.resolve({ data: [] })),
};
// Mock the QuestDB factory
mock.module('@stock-bot/questdb', () => ({
QuestDBClient: class {
constructor() {
return mockQuestdbClient;
}
},
}));
const config = {
questdb: {
enabled: true,
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
influxPort: 9009,
database: 'questdb',
},
} as any;
registerDatabaseServices(container, config);
expect(container.hasRegistration('questdbClient')).toBe(true);
});
it('should not register disabled databases', () => {
const container = createContainer();
const config = {
mongodb: { enabled: false },
postgres: { enabled: false },
questdb: undefined,
} as any;
registerDatabaseServices(container, config);
// Services are registered but with null values when disabled
expect(container.hasRegistration('mongoClient')).toBe(true);
expect(container.hasRegistration('postgresClient')).toBe(true);
expect(container.hasRegistration('questdbClient')).toBe(true);
// Verify they resolve to null
expect(container.resolve('mongoClient')).toBeNull();
expect(container.resolve('postgresClient')).toBeNull();
expect(container.resolve('questdbClient')).toBeNull();
});
});
describe('registerApplicationServices', () => {
it('should register browser when config exists', () => {
const container = createContainer();
// Mock browser factory
const mockBrowser = {
launch: mock(() => Promise.resolve()),
close: mock(() => Promise.resolve()),
};
mock.module('@stock-bot/browser', () => ({
createBrowser: () => mockBrowser,
}));
const config = {
browser: {
headless: true,
timeout: 30000,
},
} as any;
registerApplicationServices(container, config);
expect(container.hasRegistration('browser')).toBe(true);
});
it('should register proxy when config exists', () => {
const container = createContainer();
// Mock proxy factory
const mockProxy = {
getProxy: mock(() => 'http://proxy:8080'),
};
mock.module('@stock-bot/proxy', () => ({
createProxyManager: () => mockProxy,
}));
const config = {
proxy: {
enabled: true,
url: 'http://proxy:8080',
},
} as any;
registerApplicationServices(container, config);
expect(container.hasRegistration('proxyManager')).toBe(true);
});
it('should register queue manager when queue config exists', () => {
const container = createContainer();
// Mock dependencies
container.register({
cache: asValue({
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve()),
}),
handlerRegistry: asValue({
getHandler: mock(() => null),
getAllHandlers: mock(() => []),
}),
logger: asValue({
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
}),
});
// Mock queue manager
const mockQueueManager = {
getQueue: mock(() => ({})),
startAllWorkers: mock(() => {}),
shutdown: mock(() => Promise.resolve()),
};
mock.module('@stock-bot/queue', () => ({
QueueManager: class {
constructor() {
return mockQueueManager;
}
},
}));
const config = {
service: {
name: 'test-service',
serviceName: 'test-service',
},
queue: {
enabled: true,
workers: 2,
concurrency: 5,
},
} as any;
registerApplicationServices(container, config);
expect(container.hasRegistration('queueManager')).toBe(true);
});
it('should not register services when configs are missing', () => {
const container = createContainer();
const config = {} as any;
registerApplicationServices(container, config);
expect(container.hasRegistration('browser')).toBe(true);
expect(container.hasRegistration('proxyManager')).toBe(true);
expect(container.hasRegistration('queueManager')).toBe(true);
// They should be registered as null
const browser = container.resolve('browser');
const proxyManager = container.resolve('proxyManager');
const queueManager = container.resolve('queueManager');
expect(browser).toBe(null);
expect(proxyManager).toBe(null);
expect(queueManager).toBe(null);
});
});
describe('dependency resolution', () => {
it('should properly resolve cache dependencies', () => {
const container = createContainer();
const config = {
service: {
name: 'test-service',
serviceName: 'test-service',
},
redis: {
enabled: true,
host: 'localhost',
port: 6379,
db: 0,
},
} as any;
registerCacheServices(container, config);
// Should have registered cache
expect(container.hasRegistration('cache')).toBe(true);
expect(container.hasRegistration('globalCache')).toBe(true);
});
it('should handle circular dependencies gracefully', () => {
const container = createContainer();
// Register services with potential circular deps
container.register({
serviceA: asFunction(({ serviceB }) => ({ b: serviceB })).singleton(),
serviceB: asFunction(({ serviceA }) => ({ a: serviceA })).singleton(),
});
// This should throw or handle gracefully
expect(() => container.resolve('serviceA')).toThrow();
});
});
describe('registration options', () => {
it('should register services as singletons', () => {
const container = createContainer();
const config = {
browser: {
headless: true,
timeout: 30000,
},
} as any;
registerApplicationServices(container, config);
// Check that browser was registered as singleton
const registration = container.getRegistration('browser');
expect(registration).toBeDefined();
expect(registration?.lifetime).toBe('SINGLETON');
});
});
});