Files
michaelschiemer/tests/Feature/Framework/LiveComponents/ComponentCachingTest.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

233 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Status;
use App\Framework\LiveComponents\ValueObjects\CacheConfig;
describe('Component Caching Integration', function () {
it('caches component render output', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5)
);
// First request - cache miss
$response1 = $this->post('/live-component/stats:user-123', [
'method' => 'refresh',
'state' => ['views' => 100],
'cache_config' => $config->toArray(),
]);
expect($response1->status)->toBe(Status::OK);
$html1 = $response1->jsonData['html'];
// Second request - should hit cache
$response2 = $this->post('/live-component/stats:user-123', [
'method' => 'refresh',
'state' => ['views' => 100],
'cache_config' => $config->toArray(),
]);
expect($response2->status)->toBe(Status::OK);
$html2 = $response2->jsonData['html'];
// HTML should be identical (from cache)
expect($html2)->toBe($html1);
});
it('varies cache by specified parameters', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(10),
varyBy: ['category', 'page']
);
// Request with category=electronics, page=1
$response1 = $this->post('/live-component/products:filter', [
'method' => 'filter',
'state' => [
'category' => 'electronics',
'page' => 1,
'results' => [],
],
'cache_config' => $config->toArray(),
]);
// Request with category=electronics, page=2 (different page)
$response2 = $this->post('/live-component/products:filter', [
'method' => 'filter',
'state' => [
'category' => 'electronics',
'page' => 2,
'results' => [],
],
'cache_config' => $config->toArray(),
]);
// Should be different results (different cache keys)
expect($response1->jsonData['html'])->not->toBe($response2->jsonData['html']);
// Request with category=books, page=1 (different category)
$response3 = $this->post('/live-component/products:filter', [
'method' => 'filter',
'state' => [
'category' => 'books',
'page' => 1,
'results' => [],
],
'cache_config' => $config->toArray(),
]);
// Should be different from electronics
expect($response3->jsonData['html'])->not->toBe($response1->jsonData['html']);
});
it('supports stale-while-revalidate pattern', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromSeconds(1), // Short TTL
staleWhileRevalidate: true,
staleWhileRevalidateTtl: Duration::fromMinutes(10) // Long SWR TTL
);
// First request - cache miss
$response1 = $this->post('/live-component/news:feed', [
'method' => 'refresh',
'state' => ['items' => []],
'cache_config' => $config->toArray(),
]);
expect($response1->status)->toBe(Status::OK);
// Wait for TTL to expire (but within SWR window)
sleep(2);
// Second request - should serve stale content
$response2 = $this->post('/live-component/news:feed', [
'method' => 'refresh',
'state' => ['items' => []],
'cache_config' => $config->toArray(),
]);
expect($response2->status)->toBe(Status::OK);
// Should have cache-control header indicating stale
if (isset($response2->headers['Cache-Control'])) {
expect($response2->headers['Cache-Control'])->toContain('stale-while-revalidate');
}
});
it('invalidates cache on component update', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5)
);
// First render - cache
$response1 = $this->post('/live-component/counter:demo', [
'method' => 'increment',
'state' => ['count' => 0],
'cache_config' => $config->toArray(),
]);
$count1 = $response1->jsonData['state']['count'];
// Update action - should invalidate cache
$response2 = $this->post('/live-component/counter:demo', [
'method' => 'increment',
'params' => ['amount' => 10],
'state' => ['count' => $count1],
'cache_config' => $config->toArray(),
]);
$count2 = $response2->jsonData['state']['count'];
// Count should have incremented (cache invalidated)
expect($count2)->toBeGreaterThan($count1);
});
it('respects cache disabled config', function () {
$config = CacheConfig::disabled();
// First request
$response1 = $this->post('/live-component/realtime:feed', [
'method' => 'refresh',
'state' => ['timestamp' => time()],
'cache_config' => $config->toArray(),
]);
$timestamp1 = $response1->jsonData['state']['timestamp'];
sleep(1);
// Second request - should not use cache
$response2 = $this->post('/live-component/realtime:feed', [
'method' => 'refresh',
'state' => ['timestamp' => time()],
'cache_config' => $config->toArray(),
]);
$timestamp2 = $response2->jsonData['state']['timestamp'];
// Timestamps should be different (cache disabled)
expect($timestamp2)->toBeGreaterThanOrEqual($timestamp1);
});
it('caches fragments separately from full render', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5),
varyBy: ['fragments']
);
// Request full render
$fullResponse = $this->post('/live-component/card:demo', [
'method' => 'refresh',
'state' => ['title' => 'Card Title', 'content' => 'Card Content'],
'cache_config' => $config->toArray(),
]);
// Request fragment render
$fragmentResponse = $this->post('/live-component/card:demo', [
'method' => 'refresh',
'state' => ['title' => 'Card Title', 'content' => 'Card Content'],
'fragments' => ['card-header'],
'cache_config' => $config->toArray(),
]);
// Should cache both independently
expect($fullResponse->jsonData)->toHaveKey('html');
expect($fragmentResponse->jsonData)->toHaveKey('fragments');
});
it('handles cache for concurrent requests', function () {
$config = new CacheConfig(
enabled: true,
ttl: Duration::fromMinutes(5)
);
// Make multiple concurrent requests
$responses = [];
for ($i = 0; $i < 5; $i++) {
$responses[] = $this->post('/live-component/stats:global', [
'method' => 'refresh',
'state' => ['total_users' => 1000],
'cache_config' => $config->toArray(),
]);
}
// All should succeed
foreach ($responses as $response) {
expect($response->status)->toBe(Status::OK);
}
// All should have same HTML (from cache)
$firstHtml = $responses[0]->jsonData['html'];
foreach (array_slice($responses, 1) as $response) {
expect($response->jsonData['html'])->toBe($firstHtml);
}
});
});