cache = Mockery::mock(Cache::class); $this->strategy = new FragmentCacheStrategy($this->cache); }); afterEach(function () { Mockery::close(); }); describe('shouldCache()', function () { it('returns true when fragment_id is set in metadata', function () { $context = new TemplateContext( template: 'page/dashboard', data: ['stats' => [1, 2, 3]], metadata: ['fragment_id' => 'user-stats'] ); expect($this->strategy->shouldCache($context))->toBeTrue(); }); it('returns false when fragment_id is not set', function () { $context = new TemplateContext( template: 'page/dashboard', data: ['stats' => [1, 2, 3]], metadata: [] ); expect($this->strategy->shouldCache($context))->toBeFalse(); }); it('returns false when metadata is empty', function () { $context = new TemplateContext( template: 'page/home', data: ['content' => 'text'] ); expect($this->strategy->shouldCache($context))->toBeFalse(); }); it('returns true even with empty fragment_id string', function () { $context = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => ''] ); expect($this->strategy->shouldCache($context))->toBeTrue(); }); it('returns true with numeric fragment_id', function () { $context = new TemplateContext( template: 'page/report', data: [], metadata: ['fragment_id' => 123] ); expect($this->strategy->shouldCache($context))->toBeTrue(); }); it('returns true regardless of other metadata keys', function () { $context = new TemplateContext( template: 'page/profile', data: [], metadata: [ 'fragment_id' => 'user-profile', 'other_key' => 'value', 'version' => 2 ] ); expect($this->strategy->shouldCache($context))->toBeTrue(); }); }); describe('generateKey()', function () { it('generates consistent keys for same fragment', function () { $context = new TemplateContext( template: 'page/dashboard', data: ['count' => 5], metadata: ['fragment_id' => 'stats'] ); $key1 = $this->strategy->generateKey($context); $key2 = $this->strategy->generateKey($context); expect((string) $key1)->toBe((string) $key2); }); it('generates different keys for different fragment_ids', function () { $context1 = new TemplateContext( template: 'page/dashboard', data: ['count' => 5], metadata: ['fragment_id' => 'stats'] ); $context2 = new TemplateContext( template: 'page/dashboard', data: ['count' => 5], metadata: ['fragment_id' => 'chart'] ); $key1 = $this->strategy->generateKey($context1); $key2 = $this->strategy->generateKey($context2); expect((string) $key1)->not->toBe((string) $key2); }); it('generates different keys for different templates with same fragment', function () { $context1 = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => 'header'] ); $context2 = new TemplateContext( template: 'page/profile', data: [], metadata: ['fragment_id' => 'header'] ); $key1 = $this->strategy->generateKey($context1); $key2 = $this->strategy->generateKey($context2); expect((string) $key1)->not->toBe((string) $key2); }); it('generates different keys for different data', function () { $context1 = new TemplateContext( template: 'page/dashboard', data: ['count' => 5], metadata: ['fragment_id' => 'stats'] ); $context2 = new TemplateContext( template: 'page/dashboard', data: ['count' => 10], metadata: ['fragment_id' => 'stats'] ); $key1 = $this->strategy->generateKey($context1); $key2 = $this->strategy->generateKey($context2); expect((string) $key1)->not->toBe((string) $key2); }); it('uses "default" when fragment_id is missing', function () { $context = new TemplateContext( template: 'page/home', data: ['content' => 'test'], metadata: [] ); $key = $this->strategy->generateKey($context); expect((string) $key)->toContain('fragment:page/home:default:'); }); it('includes fragment_id in cache key', function () { $context = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => 'user-stats'] ); $key = $this->strategy->generateKey($context); expect((string) $key)->toContain('fragment:page/dashboard:user-stats:'); }); it('includes template name in cache key', function () { $context = new TemplateContext( template: 'reports/annual', data: [], metadata: ['fragment_id' => 'summary'] ); $key = $this->strategy->generateKey($context); expect((string) $key)->toStartWith('fragment:reports/annual:summary:'); }); it('includes data hash in cache key', function () { $context = new TemplateContext( template: 'page/dashboard', data: ['user_id' => 123, 'role' => 'admin'], metadata: ['fragment_id' => 'permissions'] ); $key = $this->strategy->generateKey($context); $keyString = (string) $key; // Key should have format: fragment:{template}:{fragment_id}:{hash} expect($keyString)->toStartWith('fragment:page/dashboard:permissions:'); expect(strlen($keyString))->toBeGreaterThan(strlen('fragment:page/dashboard:permissions:')); }); it('handles numeric fragment_id', function () { $context = new TemplateContext( template: 'page/item', data: [], metadata: ['fragment_id' => 42] ); $key = $this->strategy->generateKey($context); expect((string) $key)->toContain('fragment:page/item:42:'); }); it('generates same key for identical complex data structures', function () { $data = [ 'items' => [ ['id' => 1, 'name' => 'Item 1'], ['id' => 2, 'name' => 'Item 2'] ], 'meta' => ['total' => 2, 'page' => 1] ]; $context1 = new TemplateContext( template: 'list/items', data: $data, metadata: ['fragment_id' => 'item-list'] ); $context2 = new TemplateContext( template: 'list/items', data: $data, metadata: ['fragment_id' => 'item-list'] ); $key1 = $this->strategy->generateKey($context1); $key2 = $this->strategy->generateKey($context2); expect((string) $key1)->toBe((string) $key2); }); }); describe('getTtl()', function () { it('returns 600 seconds for all fragments', function () { $context = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => 'stats'] ); expect($this->strategy->getTtl($context))->toBe(600); }); it('returns consistent TTL for different fragments', function () { $context1 = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => 'stats'] ); $context2 = new TemplateContext( template: 'page/profile', data: [], metadata: ['fragment_id' => 'bio'] ); expect($this->strategy->getTtl($context1))->toBe(600); expect($this->strategy->getTtl($context2))->toBe(600); }); it('returns consistent TTL regardless of data', function () { $context1 = new TemplateContext( template: 'page/dashboard', data: ['small' => 'data'], metadata: ['fragment_id' => 'header'] ); $context2 = new TemplateContext( template: 'page/dashboard', data: ['large' => str_repeat('x', 1000)], metadata: ['fragment_id' => 'header'] ); expect($this->strategy->getTtl($context1))->toBe(600); expect($this->strategy->getTtl($context2))->toBe(600); }); it('returns consistent TTL when fragment_id is missing', function () { $context = new TemplateContext( template: 'page/home', data: [], metadata: [] ); expect($this->strategy->getTtl($context))->toBe(600); }); }); describe('canInvalidate()', function () { it('returns true for any template', function () { expect($this->strategy->canInvalidate('page/home'))->toBeTrue(); expect($this->strategy->canInvalidate('components/button'))->toBeTrue(); expect($this->strategy->canInvalidate('layouts/main'))->toBeTrue(); }); it('returns true for empty string', function () { expect($this->strategy->canInvalidate(''))->toBeTrue(); }); it('returns true for template with special characters', function () { expect($this->strategy->canInvalidate('admin/users/edit-profile_form'))->toBeTrue(); }); }); describe('edge cases', function () { it('handles empty data array', function () { $context = new TemplateContext( template: 'page/empty', data: [], metadata: ['fragment_id' => 'test'] ); expect($this->strategy->shouldCache($context))->toBeTrue(); $key = $this->strategy->generateKey($context); expect($key)->toBeInstanceOf(CacheKey::class); }); it('handles complex nested metadata', function () { $context = new TemplateContext( template: 'page/complex', data: ['content' => 'test'], metadata: [ 'fragment_id' => 'main-content', 'version' => 2, 'options' => ['cache' => true, 'ttl' => 300] ] ); expect($this->strategy->shouldCache($context))->toBeTrue(); $key = $this->strategy->generateKey($context); expect((string) $key)->toContain('fragment:page/complex:main-content:'); }); it('handles special characters in fragment_id', function () { $context = new TemplateContext( template: 'page/dashboard', data: [], metadata: ['fragment_id' => 'user-stats_2024-Q1'] ); $key = $this->strategy->generateKey($context); expect((string) $key)->toContain('fragment:page/dashboard:user-stats_2024-Q1:'); }); it('throws exception for very long fragment_id exceeding 250 chars', function () { $longFragmentId = str_repeat('fragment-', 50); // Results in 500 chars $context = new TemplateContext( template: 'page/test', data: [], metadata: ['fragment_id' => $longFragmentId] ); // CacheKey validates max 250 characters expect(fn() => $this->strategy->generateKey($context)) ->toThrow(\InvalidArgumentException::class, 'Cache key length exceeds maximum'); }); it('handles data with objects', function () { $context = new TemplateContext( template: 'page/dashboard', data: [ 'date' => new \DateTimeImmutable('2024-01-01 12:00:00'), 'user' => (object) ['id' => 1, 'name' => 'John'] ], metadata: ['fragment_id' => 'stats'] ); $key = $this->strategy->generateKey($context); expect($key)->toBeInstanceOf(CacheKey::class); expect((string) $key)->toStartWith('fragment:page/dashboard:stats:'); }); it('handles null fragment_id gracefully', function () { $context = new TemplateContext( template: 'page/test', data: [], metadata: ['fragment_id' => null] ); // isset() returns false for null values expect($this->strategy->shouldCache($context))->toBeFalse(); }); it('distinguishes between missing and null fragment_id', function () { $contextMissing = new TemplateContext( template: 'page/test', data: [], metadata: [] ); $contextNull = new TemplateContext( template: 'page/test', data: [], metadata: ['fragment_id' => null] ); expect($this->strategy->shouldCache($contextMissing))->toBeFalse(); expect($this->strategy->shouldCache($contextNull))->toBeFalse(); }); it('treats empty string fragment_id differently from null', function () { $context = new TemplateContext( template: 'page/test', data: [], metadata: ['fragment_id' => ''] ); // isset() returns true for empty string expect($this->strategy->shouldCache($context))->toBeTrue(); $key = $this->strategy->generateKey($context); expect((string) $key)->toStartWith('fragment:page/test::'); }); }); });