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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,347 @@
<?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:');
});
});
});

View File

@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\Strategies\FragmentCacheStrategy;
use App\Framework\View\Caching\TemplateContext;
describe('FragmentCacheStrategy', function () {
beforeEach(function () {
$this->cache = Mockery::mock(Cache::class);
$this->strategy = new FragmentCacheStrategy($this->cache);
});
afterEach(function () {
Mockery::close();
});
describe('shouldCache()', function () {
it('returns true when fragment_id is set in metadata', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: ['stats' => [1, 2, 3]],
metadata: ['fragment_id' => 'user-stats']
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
it('returns false when fragment_id is not set', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: ['stats' => [1, 2, 3]],
metadata: []
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when metadata is empty', function () {
$context = new TemplateContext(
template: 'page/home',
data: ['content' => 'text']
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns true even with empty fragment_id string', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => '']
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
it('returns true with numeric fragment_id', function () {
$context = new TemplateContext(
template: 'page/report',
data: [],
metadata: ['fragment_id' => 123]
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
it('returns true regardless of other metadata keys', function () {
$context = new TemplateContext(
template: 'page/profile',
data: [],
metadata: [
'fragment_id' => 'user-profile',
'other_key' => 'value',
'version' => 2
]
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
});
describe('generateKey()', function () {
it('generates consistent keys for same fragment', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: ['count' => 5],
metadata: ['fragment_id' => 'stats']
);
$key1 = $this->strategy->generateKey($context);
$key2 = $this->strategy->generateKey($context);
expect((string) $key1)->toBe((string) $key2);
});
it('generates different keys for different fragment_ids', function () {
$context1 = new TemplateContext(
template: 'page/dashboard',
data: ['count' => 5],
metadata: ['fragment_id' => 'stats']
);
$context2 = new TemplateContext(
template: 'page/dashboard',
data: ['count' => 5],
metadata: ['fragment_id' => 'chart']
);
$key1 = $this->strategy->generateKey($context1);
$key2 = $this->strategy->generateKey($context2);
expect((string) $key1)->not->toBe((string) $key2);
});
it('generates different keys for different templates with same fragment', function () {
$context1 = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => 'header']
);
$context2 = new TemplateContext(
template: 'page/profile',
data: [],
metadata: ['fragment_id' => 'header']
);
$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: 'page/dashboard',
data: ['count' => 5],
metadata: ['fragment_id' => 'stats']
);
$context2 = new TemplateContext(
template: 'page/dashboard',
data: ['count' => 10],
metadata: ['fragment_id' => 'stats']
);
$key1 = $this->strategy->generateKey($context1);
$key2 = $this->strategy->generateKey($context2);
expect((string) $key1)->not->toBe((string) $key2);
});
it('uses "default" when fragment_id is missing', function () {
$context = new TemplateContext(
template: 'page/home',
data: ['content' => 'test'],
metadata: []
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('fragment:page/home:default:');
});
it('includes fragment_id in cache key', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => 'user-stats']
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('fragment:page/dashboard:user-stats:');
});
it('includes template name in cache key', function () {
$context = new TemplateContext(
template: 'reports/annual',
data: [],
metadata: ['fragment_id' => 'summary']
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toStartWith('fragment:reports/annual:summary:');
});
it('includes data hash in cache key', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: ['user_id' => 123, 'role' => 'admin'],
metadata: ['fragment_id' => 'permissions']
);
$key = $this->strategy->generateKey($context);
$keyString = (string) $key;
// Key should have format: fragment:{template}:{fragment_id}:{hash}
expect($keyString)->toStartWith('fragment:page/dashboard:permissions:');
expect(strlen($keyString))->toBeGreaterThan(strlen('fragment:page/dashboard:permissions:'));
});
it('handles numeric fragment_id', function () {
$context = new TemplateContext(
template: 'page/item',
data: [],
metadata: ['fragment_id' => 42]
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('fragment:page/item:42:');
});
it('generates same key for identical complex data structures', function () {
$data = [
'items' => [
['id' => 1, 'name' => 'Item 1'],
['id' => 2, 'name' => 'Item 2']
],
'meta' => ['total' => 2, 'page' => 1]
];
$context1 = new TemplateContext(
template: 'list/items',
data: $data,
metadata: ['fragment_id' => 'item-list']
);
$context2 = new TemplateContext(
template: 'list/items',
data: $data,
metadata: ['fragment_id' => 'item-list']
);
$key1 = $this->strategy->generateKey($context1);
$key2 = $this->strategy->generateKey($context2);
expect((string) $key1)->toBe((string) $key2);
});
});
describe('getTtl()', function () {
it('returns 600 seconds for all fragments', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => 'stats']
);
expect($this->strategy->getTtl($context))->toBe(600);
});
it('returns consistent TTL for different fragments', function () {
$context1 = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => 'stats']
);
$context2 = new TemplateContext(
template: 'page/profile',
data: [],
metadata: ['fragment_id' => 'bio']
);
expect($this->strategy->getTtl($context1))->toBe(600);
expect($this->strategy->getTtl($context2))->toBe(600);
});
it('returns consistent TTL regardless of data', function () {
$context1 = new TemplateContext(
template: 'page/dashboard',
data: ['small' => 'data'],
metadata: ['fragment_id' => 'header']
);
$context2 = new TemplateContext(
template: 'page/dashboard',
data: ['large' => str_repeat('x', 1000)],
metadata: ['fragment_id' => 'header']
);
expect($this->strategy->getTtl($context1))->toBe(600);
expect($this->strategy->getTtl($context2))->toBe(600);
});
it('returns consistent TTL when fragment_id is missing', function () {
$context = new TemplateContext(
template: 'page/home',
data: [],
metadata: []
);
expect($this->strategy->getTtl($context))->toBe(600);
});
});
describe('canInvalidate()', function () {
it('returns true for any template', function () {
expect($this->strategy->canInvalidate('page/home'))->toBeTrue();
expect($this->strategy->canInvalidate('components/button'))->toBeTrue();
expect($this->strategy->canInvalidate('layouts/main'))->toBeTrue();
});
it('returns true for empty string', function () {
expect($this->strategy->canInvalidate(''))->toBeTrue();
});
it('returns true for template with special characters', function () {
expect($this->strategy->canInvalidate('admin/users/edit-profile_form'))->toBeTrue();
});
});
describe('edge cases', function () {
it('handles empty data array', function () {
$context = new TemplateContext(
template: 'page/empty',
data: [],
metadata: ['fragment_id' => 'test']
);
expect($this->strategy->shouldCache($context))->toBeTrue();
$key = $this->strategy->generateKey($context);
expect($key)->toBeInstanceOf(CacheKey::class);
});
it('handles complex nested metadata', function () {
$context = new TemplateContext(
template: 'page/complex',
data: ['content' => 'test'],
metadata: [
'fragment_id' => 'main-content',
'version' => 2,
'options' => ['cache' => true, 'ttl' => 300]
]
);
expect($this->strategy->shouldCache($context))->toBeTrue();
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('fragment:page/complex:main-content:');
});
it('handles special characters in fragment_id', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: [],
metadata: ['fragment_id' => 'user-stats_2024-Q1']
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('fragment:page/dashboard:user-stats_2024-Q1:');
});
it('throws exception for very long fragment_id exceeding 250 chars', function () {
$longFragmentId = str_repeat('fragment-', 50); // Results in 500 chars
$context = new TemplateContext(
template: 'page/test',
data: [],
metadata: ['fragment_id' => $longFragmentId]
);
// CacheKey validates max 250 characters
expect(fn() => $this->strategy->generateKey($context))
->toThrow(\InvalidArgumentException::class, 'Cache key length exceeds maximum');
});
it('handles data with objects', function () {
$context = new TemplateContext(
template: 'page/dashboard',
data: [
'date' => new \DateTimeImmutable('2024-01-01 12:00:00'),
'user' => (object) ['id' => 1, 'name' => 'John']
],
metadata: ['fragment_id' => 'stats']
);
$key = $this->strategy->generateKey($context);
expect($key)->toBeInstanceOf(CacheKey::class);
expect((string) $key)->toStartWith('fragment:page/dashboard:stats:');
});
it('handles null fragment_id gracefully', function () {
$context = new TemplateContext(
template: 'page/test',
data: [],
metadata: ['fragment_id' => null]
);
// isset() returns false for null values
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('distinguishes between missing and null fragment_id', function () {
$contextMissing = new TemplateContext(
template: 'page/test',
data: [],
metadata: []
);
$contextNull = new TemplateContext(
template: 'page/test',
data: [],
metadata: ['fragment_id' => null]
);
expect($this->strategy->shouldCache($contextMissing))->toBeFalse();
expect($this->strategy->shouldCache($contextNull))->toBeFalse();
});
it('treats empty string fragment_id differently from null', function () {
$context = new TemplateContext(
template: 'page/test',
data: [],
metadata: ['fragment_id' => '']
);
// isset() returns true for empty string
expect($this->strategy->shouldCache($context))->toBeTrue();
$key = $this->strategy->generateKey($context);
expect((string) $key)->toStartWith('fragment:page/test::');
});
});
});

View File

@@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\View\Caching\Strategies\FullPageCacheStrategy;
use App\Framework\View\Caching\TemplateContext;
describe('FullPageCacheStrategy', function () {
beforeEach(function () {
$this->cache = Mockery::mock(Cache::class);
$this->strategy = new FullPageCacheStrategy($this->cache);
});
afterEach(function () {
Mockery::close();
});
describe('shouldCache()', function () {
it('returns true for static content without user data', function () {
$context = new TemplateContext(
template: 'static-page',
data: ['title' => 'Welcome', 'content' => 'Hello World']
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
it('returns false when user data is present', function () {
$context = new TemplateContext(
template: 'user-dashboard',
data: ['user' => ['name' => 'John'], 'title' => 'Dashboard']
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when auth data is present', function () {
$context = new TemplateContext(
template: 'profile',
data: ['auth' => ['authenticated' => true]]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when session data is present', function () {
$context = new TemplateContext(
template: 'checkout',
data: ['session' => ['cart_items' => 3]]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when csrf token is present', function () {
$context = new TemplateContext(
template: 'contact-form',
data: ['csrf_token' => 'abc123']
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when flash messages are present', function () {
$context = new TemplateContext(
template: 'result-page',
data: ['flash' => ['success' => 'Saved!']]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when errors are present', function () {
$context = new TemplateContext(
template: 'form',
data: ['errors' => ['email' => 'Invalid email']]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns false when timestamp is present', function () {
$context = new TemplateContext(
template: 'news',
data: ['timestamp' => time()]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('returns true when only non-volatile data is present', function () {
$context = new TemplateContext(
template: 'product-page',
data: [
'product' => ['name' => 'Laptop', 'price' => 999],
'reviews' => [['rating' => 5]]
]
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
});
describe('generateKey()', function () {
it('generates consistent keys for same template and data', function () {
$context = new TemplateContext(
template: 'product',
data: ['id' => 123, 'name' => 'Laptop']
);
$key1 = $this->strategy->generateKey($context);
$key2 = $this->strategy->generateKey($context);
expect((string) $key1)->toBe((string) $key2);
});
it('generates different keys for different templates', function () {
$context1 = new TemplateContext(
template: 'product',
data: ['id' => 123]
);
$context2 = new TemplateContext(
template: 'category',
data: ['id' => 123]
);
$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: 'product',
data: ['id' => 123]
);
$context2 = new TemplateContext(
template: 'product',
data: ['id' => 456]
);
$key1 = $this->strategy->generateKey($context1);
$key2 = $this->strategy->generateKey($context2);
expect((string) $key1)->not->toBe((string) $key2);
});
it('generates same key when volatile data changes', function () {
$context1 = new TemplateContext(
template: 'page',
data: ['title' => 'Test', 'csrf_token' => 'abc']
);
$context2 = new TemplateContext(
template: 'page',
data: ['title' => 'Test', 'csrf_token' => 'xyz']
);
$key1 = $this->strategy->generateKey($context1);
$key2 = $this->strategy->generateKey($context2);
// CSRF token sollte ignoriert werden
expect((string) $key1)->toBe((string) $key2);
});
it('includes template name in cache key', function () {
$context = new TemplateContext(
template: 'about-page',
data: ['content' => 'About us']
);
$key = $this->strategy->generateKey($context);
expect((string) $key)->toContain('page:about-page:');
});
});
describe('getTtl()', function () {
it('returns 3600 seconds for layout templates', function () {
$context = new TemplateContext(
template: 'layouts/main',
data: []
);
expect($this->strategy->getTtl($context))->toBe(3600);
});
it('returns 7200 seconds for static templates', function () {
$context = new TemplateContext(
template: 'static/about',
data: []
);
expect($this->strategy->getTtl($context))->toBe(7200);
});
it('returns 1800 seconds for content pages', function () {
$context = new TemplateContext(
template: 'page/home',
data: []
);
expect($this->strategy->getTtl($context))->toBe(1800);
});
it('returns 900 seconds for default templates', function () {
$context = new TemplateContext(
template: 'misc-template',
data: []
);
expect($this->strategy->getTtl($context))->toBe(900);
});
});
describe('canInvalidate()', function () {
it('returns true for all templates', function () {
expect($this->strategy->canInvalidate('any-template'))->toBeTrue();
expect($this->strategy->canInvalidate('layout'))->toBeTrue();
expect($this->strategy->canInvalidate('component'))->toBeTrue();
});
});
describe('edge cases', function () {
it('handles empty data array', function () {
$context = new TemplateContext(
template: 'empty',
data: []
);
expect($this->strategy->shouldCache($context))->toBeTrue();
});
it('handles nested user data', function () {
$context = new TemplateContext(
template: 'nested',
data: [
'content' => ['text' => 'Hello'],
'user' => ['name' => 'John'] // Top-level user key
]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('handles mixed volatile and non-volatile data', function () {
$context = new TemplateContext(
template: 'mixed',
data: [
'product' => ['id' => 1],
'flash' => ['message' => 'Success']
]
);
expect($this->strategy->shouldCache($context))->toBeFalse();
});
it('generates valid cache key from complex data', function () {
$context = new TemplateContext(
template: 'complex',
data: [
'nested' => ['deep' => ['value' => 123]],
'array' => [1, 2, 3],
'string' => 'test'
]
);
$key = $this->strategy->generateKey($context);
expect($key)->toBeInstanceOf(CacheKey::class);
expect((string) $key)->toBeString();
expect(strlen((string) $key))->toBeGreaterThan(0);
});
});
});