created lots of tests

This commit is contained in:
Boki 2025-06-25 09:20:53 -04:00
parent 42baadae38
commit 54f37f9521
21 changed files with 4577 additions and 215 deletions

View file

@ -0,0 +1,46 @@
import { describe, expect, it } from 'bun:test';
import { CacheFactory } from '../src/factories';
describe('DI Factories', () => {
describe('CacheFactory', () => {
it('should be exported', () => {
expect(CacheFactory).toBeDefined();
});
it('should create cache with configuration', () => {
const cacheConfig = {
redisConfig: {
host: 'localhost',
port: 6379,
db: 1,
},
keyPrefix: 'test:',
};
const cache = CacheFactory.create(cacheConfig);
expect(cache).toBeDefined();
});
it('should create null cache without config', () => {
const cache = CacheFactory.create();
expect(cache).toBeDefined();
expect(cache.type).toBe('null');
});
it('should create cache with logger', () => {
const mockLogger = {
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
};
const cacheConfig = {
logger: mockLogger,
};
const cache = CacheFactory.create(cacheConfig);
expect(cache).toBeDefined();
});
});
})

View file

@ -0,0 +1,260 @@
import { describe, expect, it, mock, beforeEach } from 'bun:test';
import { ServiceLifecycleManager } from '../src/utils/lifecycle';
import type { AwilixContainer } from 'awilix';
describe('ServiceLifecycleManager', () => {
let manager: ServiceLifecycleManager;
beforeEach(() => {
manager = new ServiceLifecycleManager();
});
describe('initializeServices', () => {
it('should initialize services with connect method', async () => {
const mockCache = {
connect: mock(() => Promise.resolve()),
};
const mockMongoClient = {
connect: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockCache,
mongoClient: mockMongoClient,
postgresClient: null, // Not configured
},
} as unknown as AwilixContainer;
await manager.initializeServices(mockContainer);
expect(mockCache.connect).toHaveBeenCalled();
expect(mockMongoClient.connect).toHaveBeenCalled();
});
it('should initialize services with initialize method', async () => {
const mockService = {
initialize: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
await manager.initializeServices(mockContainer);
expect(mockService.initialize).toHaveBeenCalled();
});
it('should handle initialization errors', async () => {
const mockService = {
connect: mock(() => Promise.reject(new Error('Connection failed'))),
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
await expect(manager.initializeServices(mockContainer)).rejects.toThrow('Connection failed');
});
it('should handle initialization timeout', async () => {
const mockService = {
connect: mock(() => new Promise(() => {})), // Never resolves
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
await expect(manager.initializeServices(mockContainer, 100)).rejects.toThrow('cache initialization timed out after 100ms');
});
});
describe('shutdownServices', () => {
it('should shutdown services with disconnect method', async () => {
const mockCache = {
disconnect: mock(() => Promise.resolve()),
};
const mockMongoClient = {
disconnect: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockCache,
mongoClient: mockMongoClient,
},
} as unknown as AwilixContainer;
await manager.shutdownServices(mockContainer);
expect(mockCache.disconnect).toHaveBeenCalled();
expect(mockMongoClient.disconnect).toHaveBeenCalled();
});
it('should shutdown services with close method', async () => {
const mockService = {
close: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
queueManager: mockService,
},
} as unknown as AwilixContainer;
await manager.shutdownServices(mockContainer);
expect(mockService.close).toHaveBeenCalled();
});
it('should shutdown services with shutdown method', async () => {
const mockService = {
shutdown: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
await manager.shutdownServices(mockContainer);
expect(mockService.shutdown).toHaveBeenCalled();
});
it('should handle shutdown errors gracefully', async () => {
const mockService = {
disconnect: mock(() => Promise.reject(new Error('Disconnect failed'))),
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
// Should not throw
await manager.shutdownServices(mockContainer);
});
it('should shutdown services in reverse order', async () => {
const callOrder: string[] = [];
const mockCache = {
disconnect: mock(() => {
callOrder.push('cache');
return Promise.resolve();
}),
};
const mockQueueManager = {
close: mock(() => {
callOrder.push('queue');
return Promise.resolve();
}),
};
const mockContainer = {
cradle: {
cache: mockCache,
queueManager: mockQueueManager,
},
} as unknown as AwilixContainer;
await manager.shutdownServices(mockContainer);
// Queue manager should be shutdown before cache (reverse order)
expect(callOrder[0]).toBe('queue');
expect(callOrder[1]).toBe('cache');
});
});
describe('mixed lifecycle methods', () => {
it('should handle services with multiple lifecycle methods', async () => {
const mockService = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
initialize: mock(() => Promise.resolve()),
shutdown: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockService,
},
} as unknown as AwilixContainer;
// Initialize should prefer connect over initialize
await manager.initializeServices(mockContainer);
expect(mockService.connect).toHaveBeenCalled();
expect(mockService.initialize).not.toHaveBeenCalled();
// Shutdown should prefer disconnect over others
await manager.shutdownServices(mockContainer);
expect(mockService.disconnect).toHaveBeenCalled();
expect(mockService.shutdown).not.toHaveBeenCalled();
});
});
describe('complete lifecycle flow', () => {
it('should handle full initialization and shutdown cycle', async () => {
const mockCache = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
};
const mockMongoClient = {
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()),
};
const mockPostgresClient = {
connect: mock(() => Promise.resolve()),
close: mock(() => Promise.resolve()),
};
const mockQuestdbClient = {
initialize: mock(() => Promise.resolve()),
shutdown: mock(() => Promise.resolve()),
};
const mockContainer = {
cradle: {
cache: mockCache,
mongoClient: mockMongoClient,
postgresClient: mockPostgresClient,
questdbClient: mockQuestdbClient,
proxyManager: null, // Not configured
queueManager: null, // Not configured
},
} as unknown as AwilixContainer;
// Initialize all services
await manager.initializeServices(mockContainer);
expect(mockCache.connect).toHaveBeenCalled();
expect(mockMongoClient.connect).toHaveBeenCalled();
expect(mockPostgresClient.connect).toHaveBeenCalled();
expect(mockQuestdbClient.initialize).toHaveBeenCalled();
// Shutdown all services
await manager.shutdownServices(mockContainer);
expect(mockCache.disconnect).toHaveBeenCalled();
expect(mockMongoClient.disconnect).toHaveBeenCalled();
expect(mockPostgresClient.close).toHaveBeenCalled();
expect(mockQuestdbClient.shutdown).toHaveBeenCalled();
});
});
})

View file

@ -0,0 +1,273 @@
import { describe, expect, it, beforeEach, mock } from 'bun:test';
import { OperationContext } from '../src/operation-context';
import type { OperationContextOptions } from '../src/operation-context';
describe('OperationContext', () => {
const mockLogger = {
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
trace: mock(() => {}),
child: mock(() => mockLogger),
};
const mockContainer = {
resolve: mock((name: string) => ({ name })),
resolveAsync: mock(async (name: string) => ({ name })),
};
beforeEach(() => {
// Reset mocks
Object.keys(mockLogger).forEach(key => {
if (typeof mockLogger[key as keyof typeof mockLogger] === 'function') {
(mockLogger as any)[key] = mock(() =>
key === 'child' ? mockLogger : undefined
);
}
});
mockContainer.resolve = mock((name: string) => ({ name }));
mockContainer.resolveAsync = mock(async (name: string) => ({ name }));
});
describe('constructor', () => {
it('should create context with required options', () => {
const options: OperationContextOptions = {
handlerName: 'test-handler',
operationName: 'test-op',
};
const context = new OperationContext(options);
expect(context).toBeDefined();
expect(context.traceId).toBeDefined();
expect(context.metadata).toEqual({});
expect(context.logger).toBeDefined();
});
it('should create context with all options', () => {
const options: OperationContextOptions = {
handlerName: 'test-handler',
operationName: 'test-op',
parentLogger: mockLogger,
container: mockContainer,
metadata: { key: 'value' },
traceId: 'custom-trace-id',
};
const context = new OperationContext(options);
expect(context.traceId).toBe('custom-trace-id');
expect(context.metadata).toEqual({ key: 'value' });
expect(context.logger).toBe(mockLogger);
});
});
describe('static create', () => {
it('should create context using static method', () => {
const context = OperationContext.create('handler', 'operation', {
metadata: { foo: 'bar' },
});
expect(context).toBeDefined();
expect(context.metadata).toEqual({ foo: 'bar' });
});
});
describe('service resolution', () => {
it('should resolve services from container', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
container: mockContainer,
});
const service = context.resolve('myService');
expect(service).toEqual({ name: 'myService' });
expect(mockContainer.resolve).toHaveBeenCalledWith('myService');
});
it('should resolve services asynchronously', async () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
container: mockContainer,
});
const service = await context.resolveAsync('myService');
expect(service).toEqual({ name: 'myService' });
expect(mockContainer.resolveAsync).toHaveBeenCalledWith('myService');
});
it('should throw error when no container available', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
expect(() => context.resolve('service')).toThrow('No service container available');
});
it('should throw error when no container available for async', async () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
await expect(context.resolveAsync('service')).rejects.toThrow('No service container available');
});
});
describe('metadata', () => {
it('should add metadata', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
context.addMetadata('userId', '12345');
context.addMetadata('correlationId', 'corr-456');
expect(context.metadata.userId).toBe('12345');
expect(context.metadata.correlationId).toBe('corr-456');
});
it('should preserve initial metadata', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
metadata: { initial: 'value' },
});
context.addMetadata('added', 'new-value');
expect(context.metadata.initial).toBe('value');
expect(context.metadata.added).toBe('new-value');
});
});
describe('execution time', () => {
it('should track execution time', async () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 50));
const executionTime = context.getExecutionTime();
expect(executionTime).toBeGreaterThan(40);
expect(executionTime).toBeLessThan(100);
});
});
describe('logging', () => {
it('should log successful completion', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
parentLogger: mockLogger,
});
context.logCompletion(true);
expect(mockLogger.info).toHaveBeenCalledWith(
'Operation completed successfully',
expect.objectContaining({
executionTime: expect.any(Number),
metadata: {},
})
);
});
it('should log failed completion with error', () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
parentLogger: mockLogger,
});
const error = new Error('Test error');
context.logCompletion(false, error);
expect(mockLogger.error).toHaveBeenCalledWith(
'Operation failed',
expect.objectContaining({
executionTime: expect.any(Number),
error: 'Test error',
stack: expect.any(String),
metadata: {},
})
);
});
});
describe('child context', () => {
it('should create child context', () => {
const parent = new OperationContext({
handlerName: 'parent',
operationName: 'parent-op',
parentLogger: mockLogger,
container: mockContainer,
traceId: 'parent-trace',
metadata: { parentKey: 'parentValue' },
});
const child = parent.createChild('child-op', { childKey: 'childValue' });
expect(child.traceId).toBe('parent-trace'); // Inherits trace ID
expect(child.metadata).toEqual({
parentKey: 'parentValue',
childKey: 'childValue',
});
expect(child.logger).toBe(mockLogger); // Inherits logger
});
it('should create child without additional metadata', () => {
const parent = new OperationContext({
handlerName: 'parent',
operationName: 'parent-op',
metadata: { key: 'value' },
});
const child = parent.createChild('child-op');
expect(child.metadata).toEqual({ key: 'value' });
});
});
describe('dispose', () => {
it('should log completion on dispose', async () => {
const context = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
parentLogger: mockLogger,
});
await context.dispose();
expect(mockLogger.info).toHaveBeenCalledWith(
'Operation completed successfully',
expect.any(Object)
);
});
});
describe('trace ID generation', () => {
it('should generate unique trace IDs', () => {
const context1 = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
const context2 = new OperationContext({
handlerName: 'test',
operationName: 'test-op',
});
expect(context1.traceId).not.toBe(context2.traceId);
expect(context1.traceId).toMatch(/^\d+-[a-z0-9]+$/);
});
});
});

View file

@ -0,0 +1,293 @@
import { describe, expect, it, mock } from 'bun:test';
import { createContainer, asClass, asFunction, asValue } from 'awilix';
import {
registerCacheServices,
registerDatabaseServices,
registerServiceDependencies,
} from '../src/registrations';
describe('DI Registrations', () => {
describe('registerCacheServices', () => {
it('should register null cache when no redis config', () => {
const container = createContainer();
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
// No redis config
};
registerCacheServices(container, config);
const cache = container.resolve('cache');
expect(cache).toBeDefined();
expect(cache.type).toBe('null'); // NullCache type
});
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: {
host: 'localhost',
port: 6379,
db: 1,
},
};
registerCacheServices(container, config);
const cache = container.resolve('cache');
expect(cache).toBeDefined();
});
it('should register service cache', () => {
const container = createContainer();
// Register dependencies
container.register({
cache: asValue({ type: 'null' }),
config: asValue({
service: { name: 'test-service' },
redis: { host: 'localhost', port: 6379 },
}),
logger: asValue({ info: () => {} }),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
};
registerCacheServices(container, config);
const serviceCache = container.resolve('serviceCache');
expect(serviceCache).toBeDefined();
});
});
describe('registerDatabaseServices', () => {
it('should register MongoDB when config exists', () => {
const container = createContainer();
const mockLogger = {
info: () => {},
error: () => {},
warn: () => {},
debug: () => {},
};
container.register({
logger: asValue(mockLogger),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
mongodb: {
uri: 'mongodb://localhost:27017',
database: 'test-db',
},
};
registerDatabaseServices(container, config);
// Check that mongodb is registered
const registrations = container.registrations;
expect(registrations.mongodb).toBeDefined();
});
it('should register Postgres when config exists', () => {
const container = createContainer();
const mockLogger = { info: () => {}, error: () => {} };
container.register({
logger: asValue(mockLogger),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
postgres: {
host: 'localhost',
port: 5432,
database: 'test-db',
username: 'user',
password: 'pass',
},
};
registerDatabaseServices(container, config);
const registrations = container.registrations;
expect(registrations.postgres).toBeDefined();
});
it('should register QuestDB when config exists', () => {
const container = createContainer();
const mockLogger = { info: () => {}, error: () => {} };
container.register({
logger: asValue(mockLogger),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
questdb: {
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
},
};
registerDatabaseServices(container, config);
const registrations = container.registrations;
expect(registrations.questdb).toBeDefined();
});
it('should not register databases without config', () => {
const container = createContainer();
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
// No database configs
};
registerDatabaseServices(container, config);
const registrations = container.registrations;
expect(registrations.mongodb).toBeUndefined();
expect(registrations.postgres).toBeUndefined();
expect(registrations.questdb).toBeUndefined();
});
});
describe('registerServiceDependencies', () => {
it('should register browser service when config exists', () => {
const container = createContainer();
const mockLogger = { info: () => {}, error: () => {} };
container.register({
logger: asValue(mockLogger),
config: asValue({
browser: { headless: true },
}),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
browser: {
headless: true,
timeout: 30000,
},
};
registerServiceDependencies(container, config);
const registrations = container.registrations;
expect(registrations.browser).toBeDefined();
});
it('should register proxy service when config exists', () => {
const container = createContainer();
const mockLogger = { info: () => {}, error: () => {} };
container.register({
logger: asValue(mockLogger),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
proxy: {
enabled: true,
rotateOnError: true,
},
};
registerServiceDependencies(container, config);
const registrations = container.registrations;
expect(registrations.proxyManager).toBeDefined();
});
it('should register queue services for worker type', () => {
const container = createContainer();
const mockLogger = { info: () => {}, error: () => {} };
container.register({
logger: asValue(mockLogger),
config: asValue({
service: { name: 'test-service', type: 'WORKER' },
redis: { host: 'localhost', port: 6379 },
}),
});
const config = {
service: {
name: 'test-service',
type: 'WORKER' as const,
},
redis: {
host: 'localhost',
port: 6379,
},
};
registerServiceDependencies(container, config);
const registrations = container.registrations;
expect(registrations.queueManager).toBeDefined();
});
it('should not register queue for API type', () => {
const container = createContainer();
const config = {
service: {
name: 'test-api',
type: 'API' as const,
},
redis: {
host: 'localhost',
port: 6379,
},
};
registerServiceDependencies(container, config);
const registrations = container.registrations;
expect(registrations.queueManager).toBeUndefined();
});
});
})