Files
michaelschiemer/tests/Framework/LiveComponents/ComponentCacheManagerTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

283 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\LiveComponents\ComponentCacheManager;
use App\Framework\LiveComponents\Contracts\LiveComponent;
use App\Framework\LiveComponents\ValueObjects\CacheConfig;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
describe('ComponentCacheManager', function () {
beforeEach(function () {
$this->cache = Mockery::mock(Cache::class);
$this->cacheManager = new ComponentCacheManager($this->cache);
});
afterEach(function () {
Mockery::close();
});
it('caches component with basic config', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-123'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5),
varyBy: [],
staleWhileRevalidate: false
);
$html = '<div class="stats">Views: 100</div>';
$this->cache
->shouldReceive('set')
->once()
->with(Mockery::on(function (CacheItem $item) use ($html) {
return $item->value === $html
&& $item->ttl->toSeconds() === 300;
}))
->andReturn(true);
$result = $this->cacheManager->set($component, $html, $config);
expect($result)->toBeTrue();
});
it('generates cache key with varyBy parameters', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('products:filter'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([
'category' => 'electronics',
'price_min' => 100,
'price_max' => 500,
'sort' => 'price_asc',
]));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(10),
varyBy: ['category', 'price_min', 'price_max'],
staleWhileRevalidate: false
);
$html = '<div>Product list</div>';
$this->cache
->shouldReceive('set')
->once()
->with(Mockery::on(function (CacheItem $item) {
$keyString = $item->key->toString();
// Key should include component ID and varyBy parameters
return str_contains($keyString, 'products:filter')
&& str_contains($keyString, 'electronics')
&& str_contains($keyString, '100')
&& str_contains($keyString, '500');
}))
->andReturn(true);
$this->cacheManager->set($component, $html, $config);
});
it('retrieves cached component by exact key match', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-123'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['views' => 100]));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5)
);
$cachedHtml = '<div class="stats">Views: 100</div>';
$this->cache
->shouldReceive('get')
->once()
->andReturn(CacheItem::forGetting(
key: CacheKey::fromString('livecomponent:stats:user-123'),
value: $cachedHtml
));
$result = $this->cacheManager->get($component, $config);
expect($result)->toBe($cachedHtml);
});
it('returns null when cache miss', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('stats:user-999'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$config = new CacheConfig(enabled: true, ttl: Duration::fromMinutes(5));
$this->cache
->shouldReceive('get')
->once()
->andReturn(null);
$result = $this->cacheManager->get($component, $config);
expect($result)->toBeNull();
});
it('invalidates component cache', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('cart:user-123'));
$this->cache
->shouldReceive('forget')
->once()
->with(Mockery::on(function ($key) {
return str_contains($key->toString(), 'cart:user-123');
}))
->andReturn(true);
$result = $this->cacheManager->invalidate($component);
expect($result)->toBeTrue();
});
it('supports stale-while-revalidate pattern', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('news:feed'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5),
varyBy: [],
staleWhileRevalidate: true,
staleWhileRevalidateTtl: Duration::fromMinutes(60)
);
$html = '<div>News feed</div>';
$this->cache
->shouldReceive('set')
->once()
->with(Mockery::on(function (CacheItem $item) {
// SWR should use extended TTL
return $item->ttl->toSeconds() === 3600; // 60 minutes
}))
->andReturn(true);
$this->cacheManager->set($component, $html, $config);
});
it('varyBy with different values creates different cache keys', function () {
$component1 = Mockery::mock(LiveComponent::class);
$component1->shouldReceive('getId')->andReturn(ComponentId::fromString('search:results'));
$component1->shouldReceive('getData')->andReturn(ComponentData::fromArray([
'query' => 'laptop',
'page' => 1,
]));
$component2 = Mockery::mock(LiveComponent::class);
$component2->shouldReceive('getId')->andReturn(ComponentId::fromString('search:results'));
$component2->shouldReceive('getData')->andReturn(ComponentData::fromArray([
'query' => 'laptop',
'page' => 2,
]));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(10),
varyBy: ['query', 'page']
);
$capturedKeys = [];
$this->cache
->shouldReceive('set')
->twice()
->with(Mockery::on(function (CacheItem $item) use (&$capturedKeys) {
$capturedKeys[] = $item->key->toString();
return true;
}))
->andReturn(true);
$this->cacheManager->set($component1, '<div>Page 1</div>', $config);
$this->cacheManager->set($component2, '<div>Page 2</div>', $config);
// Keys should be different because page number differs
expect($capturedKeys[0])->not->toBe($capturedKeys[1]);
expect($capturedKeys[0])->toContain('laptop');
expect($capturedKeys[1])->toContain('laptop');
});
it('ignores cache when config disabled', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('realtime:feed'));
$config = new CacheConfig(enabled: false, ttl: Duration::fromMinutes(5));
$this->cache->shouldNotReceive('set');
$this->cache->shouldNotReceive('get');
$result = $this->cacheManager->set($component, '<div>Feed</div>', $config);
expect($result)->toBeFalse();
$cached = $this->cacheManager->get($component, $config);
expect($cached)->toBeNull();
});
it('handles empty varyBy array correctly', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('static:banner'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['message' => 'Welcome']));
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromHours(1),
varyBy: [] // No variation
);
$this->cache
->shouldReceive('set')
->once()
->with(Mockery::on(function (CacheItem $item) {
// Should only include component ID, no variation parameters
return str_contains($item->key->toString(), 'static:banner');
}))
->andReturn(true);
$this->cacheManager->set($component, '<div>Banner</div>', $config);
});
it('respects custom TTL durations', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('test:component'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$testCases = [
Duration::fromSeconds(30) => 30,
Duration::fromMinutes(15) => 900,
Duration::fromHours(2) => 7200,
Duration::fromDays(1) => 86400,
];
foreach ($testCases as $duration => $expectedSeconds) {
$config = new CacheConfig(enabled: true, ttl: $duration);
$this->cache
->shouldReceive('set')
->once()
->with(Mockery::on(function (CacheItem $item) use ($expectedSeconds) {
return $item->ttl->toSeconds() === $expectedSeconds;
}))
->andReturn(true);
$this->cacheManager->set($component, '<div>Test</div>', $config);
}
});
});