staticContentRatio = $staticRatio; $cacheability->hasUserSpecificContent = $hasUserContent; $cacheability->hasCsrfTokens = $hasCsrf; $cacheability->hasTimestamps = $hasTimestamps; $cacheability->hasRandomElements = $hasRandom; return new TemplateAnalysis( template: $template, recommendedStrategy: $strategy, recommendedTtl: $strategy->getTtl(), dependencies: [], cacheability: $cacheability, fragments: [] ); } describe('CacheManager', function () { beforeEach(function () { $this->cache = Mockery::mock(Cache::class); $this->analyzer = Mockery::mock(TemplateAnalyzer::class); $this->fragmentCache = Mockery::mock(FragmentCache::class); $this->cacheManager = new CacheManager( $this->cache, $this->analyzer, $this->fragmentCache ); }); afterEach(function () { Mockery::close(); }); describe('render() with cacheable strategies', function () { it('renders and caches on cache miss for FULL_PAGE strategy', function () { $context = new TemplateContext( template: 'pages/contact', data: ['email' => 'contact@example.com'] ); $analysis = createAnalysis( 'pages/contact', CacheStrategy::FULL_PAGE, staticRatio: 0.9 ); $this->analyzer->shouldReceive('analyze') ->once() ->with('pages/contact') ->andReturn($analysis); // Create actual CacheResult with cache miss $cacheKey = CacheKey::fromString('page:pages/contact:' . md5(serialize(['email' => 'contact@example.com']))); $missItem = CacheItem::miss($cacheKey); $cacheResult = CacheResult::fromItems($missItem); $this->cache->shouldReceive('get') ->once() ->andReturn($cacheResult); // Expect cache set with rendered content $renderedContent = 'Contact Page'; $this->cache->shouldReceive('set') ->once() ->with(Mockery::type(CacheItem::class)) ->andReturn(true); $result = $this->cacheManager->render($context, function () use ($renderedContent) { return $renderedContent; }); expect($result)->toBe($renderedContent); }); it('caches component templates with COMPONENT strategy', function () { $context = new TemplateContext( template: 'components/button', data: ['text' => 'Click me'] ); $analysis = createAnalysis( 'components/button', CacheStrategy::COMPONENT, staticRatio: 1.0 ); $this->analyzer->shouldReceive('analyze') ->once() ->andReturn($analysis); // Create actual CacheResult with cache miss $cacheKey = CacheKey::fromString('component:button:' . md5(serialize(['text' => 'Click me']))); $missItem = CacheItem::miss($cacheKey); $cacheResult = CacheResult::fromItems($missItem); $this->cache->shouldReceive('get')->once()->andReturn($cacheResult); $this->cache->shouldReceive('set')->once()->andReturn(true); $renderedContent = ''; $result = $this->cacheManager->render($context, fn() => $renderedContent); expect($result)->toBe($renderedContent); }); it('caches fragment with FRAGMENT strategy', function () { $context = new TemplateContext( template: 'pages/dashboard', data: ['stats' => []], metadata: ['fragment_id' => 'user-stats'] ); $analysis = createAnalysis( 'pages/dashboard', CacheStrategy::FRAGMENT, staticRatio: 0.6 ); $this->analyzer->shouldReceive('analyze') ->once() ->andReturn($analysis); // Create actual CacheResult with cache miss $cacheKey = CacheKey::fromString('fragment:pages/dashboard:user-stats:' . md5(serialize(['stats' => []]))); $missItem = CacheItem::miss($cacheKey); $cacheResult = CacheResult::fromItems($missItem); $this->cache->shouldReceive('get')->once()->andReturn($cacheResult); $this->cache->shouldReceive('set')->once()->andReturn(true); $renderedContent = '
Stats Fragment
'; $result = $this->cacheManager->render($context, fn() => $renderedContent); expect($result)->toBe($renderedContent); }); }); describe('render() with NO_CACHE strategy', function () { it('skips caching for non-cacheable content', function () { $context = new TemplateContext( template: 'pages/dynamic', data: ['user' => ['id' => 1], 'timestamp' => time()] ); $analysis = createAnalysis( 'pages/dynamic', CacheStrategy::NO_CACHE, staticRatio: 0.2, hasUserContent: true, hasTimestamps: true ); $this->analyzer->shouldReceive('analyze') ->once() ->andReturn($analysis); // No cache operations expected $this->cache->shouldReceive('get')->never(); $this->cache->shouldReceive('set')->never(); $renderedContent = 'Dynamic Content'; $result = $this->cacheManager->render($context, fn() => $renderedContent); expect($result)->toBe($renderedContent); }); }); describe('invalidateTemplate()', function () { it('invalidates cache for given template', function () { // clear() is called once per strategy that canInvalidate $this->cache->shouldReceive('clear') ->atLeast()->once() ->andReturn(true); $invalidated = $this->cacheManager->invalidateTemplate('pages/about'); // Returns count of invalidated caches expect($invalidated)->toBeGreaterThanOrEqual(0); }); it('builds correct invalidation patterns for different strategies', function () { // This test verifies the pattern building logic by invalidating $this->cache->shouldReceive('clear') ->atLeast()->once() ->andReturn(true); // Invalidate various template types $this->cacheManager->invalidateTemplate('pages/home'); $this->cacheManager->invalidateTemplate('components/button'); $this->cacheManager->invalidateTemplate('fragments/stats'); expect(true)->toBeTrue(); // Verify no exceptions thrown }); }); describe('edge cases', function () { it('handles empty template content', function () { $context = new TemplateContext( template: 'pages/empty', data: [] ); $analysis = createAnalysis( 'pages/empty', CacheStrategy::NO_CACHE ); $this->analyzer->shouldReceive('analyze') ->once() ->andReturn($analysis); $result = $this->cacheManager->render($context, fn() => ''); expect($result)->toBe(''); }); it('handles large rendered content', function () { $context = new TemplateContext( template: 'pages/large', data: [] ); $analysis = createAnalysis( 'pages/large', CacheStrategy::NO_CACHE ); $this->analyzer->shouldReceive('analyze') ->once() ->andReturn($analysis); $largeContent = str_repeat('

Content

', 1000); $result = $this->cacheManager->render($context, fn() => $largeContent); expect($result)->toBe($largeContent); expect(strlen($result))->toBeGreaterThan(10000); }); }); });