- 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.
531 lines
20 KiB
PHP
531 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\CacheResult;
|
|
use App\Framework\Core\PathProvider;
|
|
use App\Framework\View\Caching\Analysis\CacheStrategy;
|
|
use App\Framework\View\Caching\Analysis\SmartTemplateAnalyzer;
|
|
use App\Framework\View\Loading\TemplateLoader;
|
|
|
|
describe('SmartTemplateAnalyzer', function () {
|
|
beforeEach(function () {
|
|
// Create test templates directory
|
|
$this->testDir = '/home/michael/dev/michaelschiemer/tests/tmp/smart_analyzer_' . uniqid();
|
|
if (!is_dir($this->testDir)) {
|
|
mkdir($this->testDir, 0755, true);
|
|
}
|
|
|
|
// Create real PathProvider
|
|
$this->pathProvider = new PathProvider($this->testDir);
|
|
|
|
// Create mocked Cache
|
|
$this->cache = Mockery::mock(Cache::class);
|
|
$this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString('test'))));
|
|
$this->cache->shouldReceive('set')->andReturn(true);
|
|
$this->cache->shouldReceive('clear')->andReturn(true);
|
|
|
|
// Create TemplateLoader with test directory as template path
|
|
$this->loader = new TemplateLoader(
|
|
pathProvider: $this->pathProvider,
|
|
cache: $this->cache,
|
|
templates: [],
|
|
templatePath: '', // Empty to use base path directly
|
|
cacheEnabled: false
|
|
);
|
|
|
|
$this->analyzer = new SmartTemplateAnalyzer($this->loader);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
|
|
// Clean up test files
|
|
if (is_dir($this->testDir)) {
|
|
$files = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($this->testDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
foreach ($files as $file) {
|
|
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
|
|
}
|
|
rmdir($this->testDir);
|
|
}
|
|
});
|
|
|
|
describe('analyze()', function () {
|
|
it('recommends FULL_PAGE strategy for layouts with high static ratio', function () {
|
|
$content = '<html><head><title>Site</title></head><body>Mostly static content here</body></html>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/layouts/main.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('layouts/main');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
|
|
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.8);
|
|
});
|
|
|
|
it('recommends COMPONENT strategy for component templates', function () {
|
|
$content = '<button class="btn">{{ text }}</button>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/components/button.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('components/button');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
|
|
});
|
|
|
|
it('recommends COMPONENT strategy for partial templates', function () {
|
|
$content = '<nav class="main-navigation"><ul class="nav-list">{{ navigation_items }}</ul></nav>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/partials/navigation.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('partials/navigation');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
|
|
});
|
|
|
|
it('recommends NO_CACHE for templates with user content', function () {
|
|
$content = '<div>Welcome {{ user.name }}! <p>Your profile</p></div>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/pages/profile.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('pages/profile');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
|
|
expect($analysis->cacheability->hasUserSpecificContent)->toBeTrue();
|
|
});
|
|
|
|
it('detects CSRF tokens and adjusts cacheability score', function () {
|
|
$content = '<form method="POST"><input type="hidden" name="_token" value="{{ csrf_token }}"></form>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/forms/contact.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('forms/contact');
|
|
|
|
// CSRF tokens detected but high static ratio (0.839) means still cacheable (0.839 - 0.2 = 0.639 > 0.5)
|
|
expect($analysis->cacheability->hasCsrfTokens)->toBeTrue();
|
|
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('detects timestamps and adjusts cacheability score', function () {
|
|
$content = '<div>Current time: {{ now }}</div>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/widgets/clock.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('widgets/clock');
|
|
|
|
// Timestamps detected but high static ratio means still cacheable
|
|
expect($analysis->cacheability->hasTimestamps)->toBeTrue();
|
|
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('recommends NO_CACHE for templates with random elements', function () {
|
|
$content = '<div>Random ID: {{ uuid }}</div>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/widgets/random.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('widgets/random');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
|
|
expect($analysis->cacheability->hasRandomElements)->toBeTrue();
|
|
});
|
|
|
|
it('recommends FRAGMENT strategy for medium cacheability templates', function () {
|
|
$content = '<div class="content"><h1>Title</h1><p>Some content</p><span>{{ small_dynamic_part }}</span></div>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/pages/mixed.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('pages/mixed');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FRAGMENT);
|
|
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('creates fallback analysis when loader throws exception', function () {
|
|
$analysis = $this->analyzer->analyze('missing/template');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
|
|
expect($analysis->recommendedTtl)->toBe(0);
|
|
expect($analysis->dependencies)->toBeEmpty();
|
|
});
|
|
|
|
it('adjusts TTL based on cacheability score', function () {
|
|
$staticContent = '<html><body>All static content here</body></html>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/static/page.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $staticContent);
|
|
|
|
$analysis = $this->analyzer->analyze('static/page');
|
|
|
|
expect($analysis->recommendedTtl)->toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('getDependencies()', function () {
|
|
it('finds @include directives', function () {
|
|
$content = '@include("header") <div>Content</div> @include("footer")';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/page/test.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$deps = $this->analyzer->getDependencies('page/test');
|
|
|
|
expect($deps)->toHaveKey('includes');
|
|
expect($deps['includes'])->toContain('header');
|
|
expect($deps['includes'])->toContain('footer');
|
|
});
|
|
|
|
it('finds <component> tags', function () {
|
|
$content = '<component name="button" /> <component name="card" />';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/page/test2.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$deps = $this->analyzer->getDependencies('page/test2');
|
|
|
|
expect($deps)->toHaveKey('components');
|
|
expect($deps['components'])->toContain('button');
|
|
expect($deps['components'])->toContain('card');
|
|
});
|
|
|
|
it('finds both includes and components', function () {
|
|
$content = '@include("layout") <component name="nav" /> <component name="footer" />';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/page/test3.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$deps = $this->analyzer->getDependencies('page/test3');
|
|
|
|
expect($deps)->toHaveKey('includes');
|
|
expect($deps)->toHaveKey('components');
|
|
expect($deps['includes'])->toContain('layout');
|
|
expect($deps['components'])->toContain('nav');
|
|
});
|
|
|
|
it('returns empty array when no dependencies found', function () {
|
|
$content = '<div>No dependencies here</div>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/page/simple.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$deps = $this->analyzer->getDependencies('page/simple');
|
|
|
|
expect($deps)->toBeEmpty();
|
|
});
|
|
|
|
it('returns empty array when loader throws exception', function () {
|
|
$deps = $this->analyzer->getDependencies('page/error');
|
|
|
|
expect($deps)->toBeEmpty();
|
|
});
|
|
});
|
|
|
|
describe('getCacheability()', function () {
|
|
it('detects user-specific content patterns', function () {
|
|
$content = '{{ user.name }} {{ current_user.email }} {{ auth.role }}';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/user.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/user');
|
|
|
|
expect($score->hasUserSpecificContent)->toBeTrue();
|
|
expect($score->isCacheable())->toBeFalse();
|
|
});
|
|
|
|
it('detects CSRF token patterns', function () {
|
|
$content = '{{ csrf_token }} {{ _token }}';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/csrf.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/csrf');
|
|
|
|
expect($score->hasCsrfTokens)->toBeTrue();
|
|
expect($score->isCacheable())->toBeFalse();
|
|
});
|
|
|
|
it('detects timestamp patterns', function () {
|
|
$content = '{{ now }} {{ timestamp }} {{ date() }}';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/time.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/time');
|
|
|
|
expect($score->hasTimestamps)->toBeTrue();
|
|
expect($score->isCacheable())->toBeFalse();
|
|
});
|
|
|
|
it('detects random element patterns', function () {
|
|
$content = '{{ random }} {{ rand() }} {{ uuid }}';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/random.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/random');
|
|
|
|
expect($score->hasRandomElements)->toBeTrue();
|
|
expect($score->isCacheable())->toBeFalse();
|
|
});
|
|
|
|
it('calculates high static content ratio for mostly static templates', function () {
|
|
$content = '<div>Static content</div><p>More static content here with lots of text</p><section>Even more static HTML content</section>{{ small_dynamic }}';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/static.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/static');
|
|
|
|
expect($score->staticContentRatio)->toBeGreaterThan(0.8);
|
|
expect($score->isCacheable())->toBeTrue();
|
|
});
|
|
|
|
it('calculates low static content ratio for mostly dynamic templates', function () {
|
|
$content = '{{ var1 }} {{ var2 }} {{ var3 }} {{ var4 }} <p>static</p>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/test/dynamic.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$score = $this->analyzer->getCacheability('test/dynamic');
|
|
|
|
expect($score->staticContentRatio)->toBeLessThan(0.5);
|
|
});
|
|
|
|
it('returns default score when loader throws exception', function () {
|
|
$score = $this->analyzer->getCacheability('test/error');
|
|
|
|
// Default CacheabilityScore has staticContentRatio = 0.0
|
|
expect($score->staticContentRatio)->toBe(0.0);
|
|
expect($score->hasUserSpecificContent)->toBeFalse();
|
|
expect($score->hasCsrfTokens)->toBeFalse();
|
|
expect($score->hasTimestamps)->toBeFalse();
|
|
expect($score->hasRandomElements)->toBeFalse();
|
|
});
|
|
|
|
it('handles empty content gracefully', function () {
|
|
// Create empty template file
|
|
$filePath = $this->testDir . '/test/empty.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, '');
|
|
|
|
$score = $this->analyzer->getCacheability('test/empty');
|
|
|
|
expect($score->staticContentRatio)->toBe(0.0);
|
|
});
|
|
});
|
|
|
|
describe('integration scenarios', function () {
|
|
it('correctly analyzes a typical blog post template', function () {
|
|
$content = <<<HTML
|
|
<article class="post">
|
|
<header>
|
|
<h1>{{ post.title }}</h1>
|
|
<time>{{ post.published_at }}</time>
|
|
</header>
|
|
<div class="content">
|
|
{{ post.content }}
|
|
</div>
|
|
<footer>
|
|
<p>Read more articles</p>
|
|
</footer>
|
|
</article>
|
|
HTML;
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/blog/post.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('blog/post');
|
|
|
|
expect($analysis->cacheability->hasTimestamps)->toBeFalse();
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FRAGMENT);
|
|
});
|
|
|
|
it('correctly analyzes a static landing page', function () {
|
|
$content = <<<HTML
|
|
<html>
|
|
<head><title>Welcome</title></head>
|
|
<body>
|
|
<header><h1>Welcome to our site</h1></header>
|
|
<main>
|
|
<section>Static marketing content</section>
|
|
<section>More static content</section>
|
|
</main>
|
|
<footer>Copyright 2024</footer>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/pages/landing.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('pages/landing');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
|
|
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.9);
|
|
});
|
|
|
|
it('correctly analyzes a reusable button component', function () {
|
|
$content = '<button class="btn btn-{{ variant }}">{{ text }}</button>';
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/components/button2.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('components/button2');
|
|
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
|
|
expect($analysis->recommendedTtl)->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('correctly analyzes a form with CSRF protection', function () {
|
|
$content = <<<HTML
|
|
<form method="POST" action="/submit">
|
|
{{ csrf_token }}
|
|
<input type="text" name="email" />
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
HTML;
|
|
|
|
// Create template file
|
|
$filePath = $this->testDir . '/forms/contact2.view.php';
|
|
$dir = dirname($filePath);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
|
|
$analysis = $this->analyzer->analyze('forms/contact2');
|
|
|
|
// CSRF detected and high static ratio means still cacheable, recommends FULL_PAGE
|
|
expect($analysis->cacheability->hasCsrfTokens)->toBeTrue();
|
|
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.8);
|
|
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
|
|
});
|
|
});
|
|
});
|