- 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.
233 lines
7.7 KiB
PHP
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);
|
|
}
|
|
});
|
|
});
|