- 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.
348 lines
12 KiB
PHP
348 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\View\Caching\Strategies\ComponentCacheStrategy;
|
|
use App\Framework\View\Caching\TemplateContext;
|
|
|
|
describe('ComponentCacheStrategy', function () {
|
|
beforeEach(function () {
|
|
$this->cache = Mockery::mock(Cache::class);
|
|
$this->strategy = new ComponentCacheStrategy($this->cache);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('shouldCache()', function () {
|
|
it('returns true for templates with "component" in name', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['text' => 'Click me']
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
});
|
|
|
|
it('returns true for templates with "partial" in name', function () {
|
|
$context = new TemplateContext(
|
|
template: 'partials/navigation',
|
|
data: ['items' => []]
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
});
|
|
|
|
it('returns true for nested component paths', function () {
|
|
$context = new TemplateContext(
|
|
template: 'views/components/card/header',
|
|
data: ['title' => 'Header']
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
});
|
|
|
|
it('returns true for partial in subdirectory', function () {
|
|
$context = new TemplateContext(
|
|
template: 'admin/partials/sidebar',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
});
|
|
|
|
it('returns false for regular templates', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/home',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeFalse();
|
|
});
|
|
|
|
it('returns false for layouts', function () {
|
|
$context = new TemplateContext(
|
|
template: 'layouts/main',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeFalse();
|
|
});
|
|
|
|
it('is case sensitive for component keyword', function () {
|
|
$context = new TemplateContext(
|
|
template: 'COMPONENTS/button', // Uppercase
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('generateKey()', function () {
|
|
it('generates consistent keys for same component and data', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['text' => 'Click', 'variant' => 'primary']
|
|
);
|
|
|
|
$key1 = $this->strategy->generateKey($context);
|
|
$key2 = $this->strategy->generateKey($context);
|
|
|
|
expect((string) $key1)->toBe((string) $key2);
|
|
});
|
|
|
|
it('generates different keys for different components', function () {
|
|
$context1 = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['text' => 'Click']
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'components/link',
|
|
data: ['text' => 'Click']
|
|
);
|
|
|
|
$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: 'components/button',
|
|
data: ['text' => 'Submit']
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['text' => 'Cancel']
|
|
);
|
|
|
|
$key1 = $this->strategy->generateKey($context1);
|
|
$key2 = $this->strategy->generateKey($context2);
|
|
|
|
expect((string) $key1)->not->toBe((string) $key2);
|
|
});
|
|
|
|
it('uses basename for nested component paths', function () {
|
|
$context1 = new TemplateContext(
|
|
template: 'views/components/button',
|
|
data: ['text' => 'Click']
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'other/components/button',
|
|
data: ['text' => 'Click']
|
|
);
|
|
|
|
$key1 = $this->strategy->generateKey($context1);
|
|
$key2 = $this->strategy->generateKey($context2);
|
|
|
|
// Same basename and data = same key
|
|
expect((string) $key1)->toBe((string) $key2);
|
|
});
|
|
|
|
it('includes component basename in cache key', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/alert',
|
|
data: ['message' => 'Success']
|
|
);
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
|
|
expect((string) $key)->toContain('component:alert:');
|
|
});
|
|
|
|
it('includes data hash in cache key', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/card',
|
|
data: ['title' => 'Test', 'content' => 'Content']
|
|
);
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
$keyString = (string) $key;
|
|
|
|
// Key should have format: component:card:{hash}
|
|
expect($keyString)->toStartWith('component:card:');
|
|
expect(strlen($keyString))->toBeGreaterThan(strlen('component:card:'));
|
|
});
|
|
|
|
it('generates same key for identical nested data structures', function () {
|
|
$data = [
|
|
'user' => ['name' => 'John', 'role' => 'admin'],
|
|
'settings' => ['theme' => 'dark']
|
|
];
|
|
|
|
$context1 = new TemplateContext(
|
|
template: 'components/profile',
|
|
data: $data
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'components/profile',
|
|
data: $data
|
|
);
|
|
|
|
$key1 = $this->strategy->generateKey($context1);
|
|
$key2 = $this->strategy->generateKey($context2);
|
|
|
|
expect((string) $key1)->toBe((string) $key2);
|
|
});
|
|
});
|
|
|
|
describe('getTtl()', function () {
|
|
it('returns 1800 seconds for all components', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/button',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->getTtl($context))->toBe(1800);
|
|
});
|
|
|
|
it('returns consistent TTL for different components', function () {
|
|
$context1 = new TemplateContext(
|
|
template: 'components/card',
|
|
data: []
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'partials/header',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->getTtl($context1))->toBe(1800);
|
|
expect($this->strategy->getTtl($context2))->toBe(1800);
|
|
});
|
|
|
|
it('returns consistent TTL regardless of data', function () {
|
|
$context1 = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['small' => 'data']
|
|
);
|
|
$context2 = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['large' => str_repeat('x', 1000)]
|
|
);
|
|
|
|
expect($this->strategy->getTtl($context1))->toBe(1800);
|
|
expect($this->strategy->getTtl($context2))->toBe(1800);
|
|
});
|
|
});
|
|
|
|
describe('canInvalidate()', function () {
|
|
it('returns true for component templates', function () {
|
|
expect($this->strategy->canInvalidate('components/button'))->toBeTrue();
|
|
});
|
|
|
|
it('returns true for partial templates', function () {
|
|
expect($this->strategy->canInvalidate('partials/navigation'))->toBeTrue();
|
|
});
|
|
|
|
it('returns true for nested component paths', function () {
|
|
expect($this->strategy->canInvalidate('views/components/card'))->toBeTrue();
|
|
});
|
|
|
|
it('returns false for regular templates', function () {
|
|
expect($this->strategy->canInvalidate('pages/home'))->toBeFalse();
|
|
});
|
|
|
|
it('returns false for layouts', function () {
|
|
expect($this->strategy->canInvalidate('layouts/main'))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('edge cases', function () {
|
|
it('handles empty data array', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/divider',
|
|
data: []
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
expect($key)->toBeInstanceOf(CacheKey::class);
|
|
});
|
|
|
|
it('handles complex nested data structures', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/table',
|
|
data: [
|
|
'headers' => ['Name', 'Email', 'Role'],
|
|
'rows' => [
|
|
['John Doe', 'john@example.com', 'admin'],
|
|
['Jane Smith', 'jane@example.com', 'user']
|
|
],
|
|
'pagination' => [
|
|
'page' => 1,
|
|
'total' => 10,
|
|
'perPage' => 20
|
|
]
|
|
]
|
|
);
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
expect($key)->toBeInstanceOf(CacheKey::class);
|
|
expect((string) $key)->toBeString();
|
|
expect(strlen((string) $key))->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('handles special characters in component name', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/user-profile_card',
|
|
data: ['user_id' => 123]
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
expect((string) $key)->toContain('component:user-profile_card:');
|
|
});
|
|
|
|
it('treats "component" and "partial" equally for caching', function () {
|
|
$componentContext = new TemplateContext(
|
|
template: 'components/item',
|
|
data: ['id' => 1]
|
|
);
|
|
$partialContext = new TemplateContext(
|
|
template: 'partials/item',
|
|
data: ['id' => 1]
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($componentContext))->toBeTrue();
|
|
expect($this->strategy->shouldCache($partialContext))->toBeTrue();
|
|
expect($this->strategy->getTtl($componentContext))->toBe(
|
|
$this->strategy->getTtl($partialContext)
|
|
);
|
|
});
|
|
|
|
it('handles component path with multiple segments', function () {
|
|
$context = new TemplateContext(
|
|
template: 'admin/dashboard/components/widget/chart',
|
|
data: ['data' => [1, 2, 3]]
|
|
);
|
|
|
|
expect($this->strategy->shouldCache($context))->toBeTrue();
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
// Basename should be 'chart'
|
|
expect((string) $key)->toContain('component:chart:');
|
|
});
|
|
|
|
it('generates valid cache key from data with objects', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/user-card',
|
|
data: [
|
|
'user' => (object) ['name' => 'John', 'email' => 'john@example.com'],
|
|
'timestamp' => new \DateTimeImmutable('2024-01-01 12:00:00')
|
|
]
|
|
);
|
|
|
|
$key = $this->strategy->generateKey($context);
|
|
expect($key)->toBeInstanceOf(CacheKey::class);
|
|
expect((string) $key)->toStartWith('component:user-card:');
|
|
});
|
|
});
|
|
});
|