273 lines
7.9 KiB
TypeScript
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]+$/);
|
|
});
|
|
});
|
|
});
|