Files
michaelschiemer/tests/Unit/Framework/View/Caching/Strategies/ComponentCacheStrategyTest.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

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:');
});
});
});