structured)->toBe([]); expect($context->tags)->toBe([]); expect($context->trace)->toBeNull(); expect($context->user)->toBeNull(); expect($context->request)->toBeNull(); expect($context->metadata)->toBe([]); }); }); describe('withData()', function () { it('creates LogContext with structured data', function () { $data = ['user_id' => 123, 'action' => 'login']; $context = LogContext::withData($data); expect($context->structured)->toBe($data); expect($context->tags)->toBe([]); }); }); describe('withTags()', function () { it('creates LogContext with tags', function () { $context = LogContext::withTags('security', 'authentication'); expect($context->tags)->toBe(['security', 'authentication']); expect($context->structured)->toBe([]); }); it('accepts variadic tags', function () { $context = LogContext::withTags('tag1', 'tag2', 'tag3'); expect($context->tags)->toHaveCount(3); expect($context->tags)->toContain('tag1', 'tag2', 'tag3'); }); }); describe('addData()', function () { it('adds single data entry', function () { $context = LogContext::empty()->addData('key', 'value'); expect($context->structured)->toBe(['key' => 'value']); }); it('preserves existing data when adding new', function () { $context = LogContext::withData(['existing' => 'data']) ->addData('new', 'value'); expect($context->structured)->toBe([ 'existing' => 'data', 'new' => 'value', ]); }); it('returns new instance (immutability)', function () { $original = LogContext::withData(['original' => 'data']); $modified = $original->addData('new', 'value'); expect($original->structured)->toBe(['original' => 'data']); expect($modified->structured)->toBe([ 'original' => 'data', 'new' => 'value', ]); expect($original)->not->toBe($modified); }); }); describe('mergeData()', function () { it('merges multiple data entries', function () { $context = LogContext::withData(['existing' => 'data']) ->mergeData(['new1' => 'value1', 'new2' => 'value2']); expect($context->structured)->toBe([ 'existing' => 'data', 'new1' => 'value1', 'new2' => 'value2', ]); }); it('overwrites existing keys', function () { $context = LogContext::withData(['key' => 'old']) ->mergeData(['key' => 'new']); expect($context->structured['key'])->toBe('new'); }); }); describe('addTags()', function () { it('adds tags to existing tags', function () { $context = LogContext::withTags('tag1') ->addTags('tag2', 'tag3'); expect($context->tags)->toHaveCount(3); expect($context->tags)->toContain('tag1', 'tag2', 'tag3'); }); it('removes duplicate tags', function () { $context = LogContext::withTags('tag1', 'tag2') ->addTags('tag2', 'tag3'); expect($context->tags)->toHaveCount(3); expect($context->tags)->toContain('tag1', 'tag2', 'tag3'); }); it('returns new instance (immutability)', function () { $original = LogContext::withTags('tag1'); $modified = $original->addTags('tag2'); expect($original->tags)->toBe(['tag1']); expect($modified->tags)->toHaveCount(2); expect($original)->not->toBe($modified); }); }); describe('withTrace()', function () { it('sets trace context', function () { $random = new SecureRandomGenerator(); $trace = TraceContext::start($random, 'trace-id-123'); $context = LogContext::empty()->withTrace($trace); expect($context->trace)->toBe($trace); // Cleanup TraceContext::clear(); }); }); describe('withUser()', function () { it('sets user context', function () { $user = new UserContext('user-123', 'john@example.com'); $context = LogContext::empty()->withUser($user); expect($context->user)->toBe($user); }); }); describe('withRequest()', function () { it('sets request context', function () { $request = RequestContext::empty(); $context = LogContext::empty()->withRequest($request); expect($context->request)->toBe($request); }); }); describe('addMetadata()', function () { it('adds metadata entry', function () { $context = LogContext::empty()->addMetadata('key', 'value'); expect($context->metadata)->toBe(['key' => 'value']); }); it('preserves existing metadata', function () { $context = LogContext::empty() ->addMetadata('key1', 'value1') ->addMetadata('key2', 'value2'); expect($context->metadata)->toBe([ 'key1' => 'value1', 'key2' => 'value2', ]); }); }); describe('merge()', function () { it('merges two LogContexts', function () { $context1 = LogContext::withData(['key1' => 'value1']) ->addTags('tag1'); $context2 = LogContext::withData(['key2' => 'value2']) ->addTags('tag2'); $merged = $context1->merge($context2); expect($merged->structured)->toBe([ 'key1' => 'value1', 'key2' => 'value2', ]); expect($merged->tags)->toContain('tag1', 'tag2'); }); it('overwrites structured data on merge', function () { $context1 = LogContext::withData(['key' => 'old']); $context2 = LogContext::withData(['key' => 'new']); $merged = $context1->merge($context2); expect($merged->structured['key'])->toBe('new'); }); it('removes duplicate tags on merge', function () { $context1 = LogContext::withTags('tag1', 'tag2'); $context2 = LogContext::withTags('tag2', 'tag3'); $merged = $context1->merge($context2); expect($merged->tags)->toHaveCount(3); expect($merged->tags)->toContain('tag1', 'tag2', 'tag3'); }); it('prefers other context for trace/user/request', function () { $random = new SecureRandomGenerator(); $trace1 = TraceContext::start($random, 'trace-1'); TraceContext::clear(); $trace2 = TraceContext::start($random, 'trace-2'); $user1 = new UserContext('user-1', 'user1@example.com'); $user2 = new UserContext('user-2', 'user2@example.com'); $context1 = LogContext::empty() ->withTrace($trace1) ->withUser($user1); $context2 = LogContext::empty() ->withTrace($trace2) ->withUser($user2); $merged = $context1->merge($context2); expect($merged->trace)->toBe($trace2); expect($merged->user)->toBe($user2); // Cleanup TraceContext::clear(); }); it('keeps original trace/user/request when other is null', function () { $random = new SecureRandomGenerator(); $trace = TraceContext::start($random, 'trace-1'); $user = new UserContext('user-1', 'user1@example.com'); $context1 = LogContext::empty() ->withTrace($trace) ->withUser($user); $context2 = LogContext::withData(['key' => 'value']); $merged = $context1->merge($context2); expect($merged->trace)->toBe($trace); expect($merged->user)->toBe($user); // Cleanup TraceContext::clear(); }); }); describe('hasStructuredData()', function () { it('returns true when structured data exists', function () { $context = LogContext::withData(['key' => 'value']); expect($context->hasStructuredData())->toBeTrue(); }); it('returns false when no structured data', function () { $context = LogContext::empty(); expect($context->hasStructuredData())->toBeFalse(); }); }); describe('hasTags()', function () { it('returns true when tags exist', function () { $context = LogContext::withTags('tag1'); expect($context->hasTags())->toBeTrue(); }); it('returns false when no tags', function () { $context = LogContext::empty(); expect($context->hasTags())->toBeFalse(); }); }); describe('hasTag()', function () { it('returns true when specific tag exists', function () { $context = LogContext::withTags('security', 'auth'); expect($context->hasTag('security'))->toBeTrue(); expect($context->hasTag('auth'))->toBeTrue(); }); it('returns false when tag does not exist', function () { $context = LogContext::withTags('security'); expect($context->hasTag('performance'))->toBeFalse(); }); it('uses strict comparison', function () { $context = LogContext::withTags('123'); expect($context->hasTag('123'))->toBeTrue(); // hasTag expects string, so we don't test with int }); }); describe('toArray()', function () { it('returns empty array for empty context', function () { $context = LogContext::empty(); expect($context->toArray())->toBe([]); }); it('includes structured data when present', function () { $context = LogContext::withData(['key' => 'value']); $array = $context->toArray(); expect($array)->toHaveKey('structured'); expect($array['structured'])->toBe(['key' => 'value']); }); it('includes tags when present', function () { $context = LogContext::withTags('tag1', 'tag2'); $array = $context->toArray(); expect($array)->toHaveKey('tags'); expect($array['tags'])->toBe(['tag1', 'tag2']); }); it('includes trace when present', function () { $random = new SecureRandomGenerator(); $trace = TraceContext::start($random, 'trace-123'); $context = LogContext::empty()->withTrace($trace); $array = $context->toArray(); expect($array)->toHaveKey('trace'); expect($array['trace'])->toHaveKey('trace_id'); expect($array['trace']['trace_id'])->toBe('trace-123'); // Cleanup TraceContext::clear(); }); it('includes user when present', function () { $user = new UserContext('user-123', 'john@example.com'); $context = LogContext::empty()->withUser($user); $array = $context->toArray(); expect($array)->toHaveKey('user'); expect($array['user'])->toBeArray(); }); it('includes request when present', function () { $request = RequestContext::empty(); $context = LogContext::empty()->withRequest($request); $array = $context->toArray(); expect($array)->toHaveKey('request'); expect($array['request'])->toBeArray(); }); it('includes metadata when present', function () { $context = LogContext::empty()->addMetadata('key', 'value'); $array = $context->toArray(); expect($array)->toHaveKey('metadata'); expect($array['metadata'])->toBe(['key' => 'value']); }); it('includes all sections when all are present', function () { $random = new SecureRandomGenerator(); $trace = TraceContext::start($random, 'trace-123'); $user = new UserContext('user-123', 'john@example.com'); $request = RequestContext::empty(); $context = LogContext::withData(['key' => 'value']) ->addTags('security') ->withTrace($trace) ->withUser($user) ->withRequest($request) ->addMetadata('meta', 'data'); $array = $context->toArray(); expect($array)->toHaveKeys(['structured', 'tags', 'trace', 'user', 'request', 'metadata']); // Cleanup TraceContext::clear(); }); }); describe('immutability', function () { it('is immutable through fluent API', function () { $original = LogContext::empty(); $modified = $original ->addData('key', 'value') ->addTags('tag') ->addMetadata('meta', 'value'); // Original remains unchanged expect($original->structured)->toBe([]); expect($original->tags)->toBe([]); expect($original->metadata)->toBe([]); // Modified has all changes expect($modified->structured)->toBe(['key' => 'value']); expect($modified->tags)->toBe(['tag']); expect($modified->metadata)->toBe(['meta' => 'value']); }); }); });