stock-bot/libs/core/di/test/operation-context.test.ts
2025-06-25 11:38:23 -04:00

273 lines
7.9 KiB
TypeScript

import { beforeEach, describe, expect, it, 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]+$/);
});
});
});