- 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.
383 lines
13 KiB
PHP
383 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\CacheIdentifier;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\CachePrefix;
|
|
use App\Framework\Cache\CacheResult;
|
|
use App\Framework\Cache\CacheTag;
|
|
use App\Framework\Cache\Compression\NoCompression;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Cache\SmartCache;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Serializer\Php\PhpSerializer;
|
|
|
|
describe('SmartCache', function () {
|
|
beforeEach(function () {
|
|
$driver = new InMemoryCache();
|
|
$serializer = new PhpSerializer();
|
|
$compression = new NoCompression();
|
|
$this->innerCache = new GeneralCache($driver, $serializer, $compression);
|
|
$this->cache = new SmartCache($this->innerCache);
|
|
});
|
|
|
|
describe('Basic Cache Operations', function () {
|
|
it('sets and gets a single cache item', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
$value = 'test-value';
|
|
|
|
$item = CacheItem::forSet($key, $value, Duration::fromMinutes(5));
|
|
$this->cache->set($item);
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBe('test-value');
|
|
expect($result->getItem($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('returns miss for non-existent key', function () {
|
|
$key = CacheKey::fromString('non-existent');
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeFalse();
|
|
expect($result->value)->toBeNull();
|
|
});
|
|
|
|
it('sets multiple cache items in batch', function () {
|
|
$item1 = CacheItem::forSet(
|
|
CacheKey::fromString('key1'),
|
|
'value1',
|
|
Duration::fromMinutes(5)
|
|
);
|
|
$item2 = CacheItem::forSet(
|
|
CacheKey::fromString('key2'),
|
|
'value2',
|
|
Duration::fromMinutes(5)
|
|
);
|
|
|
|
$success = $this->cache->set($item1, $item2);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
$result = $this->cache->get(
|
|
CacheKey::fromString('key1'),
|
|
CacheKey::fromString('key2')
|
|
);
|
|
|
|
expect($result->count())->toBe(2);
|
|
expect($result->isCompleteHit())->toBeTrue();
|
|
});
|
|
|
|
it('checks if cache keys exist', function () {
|
|
$key1 = CacheKey::fromString('exists');
|
|
$key2 = CacheKey::fromString('not-exists');
|
|
|
|
$this->cache->set(CacheItem::forSet($key1, 'value'));
|
|
|
|
$results = $this->cache->has($key1, $key2);
|
|
|
|
expect($results['exists'])->toBeTrue();
|
|
expect($results['not-exists'])->toBeFalse();
|
|
});
|
|
|
|
it('forgets cache items', function () {
|
|
$key = CacheKey::fromString('to-forget');
|
|
|
|
$this->cache->set(CacheItem::forSet($key, 'value'));
|
|
expect($this->cache->get($key)->isHit)->toBeTrue();
|
|
|
|
$this->cache->forget($key);
|
|
|
|
expect($this->cache->get($key)->isHit)->toBeFalse();
|
|
});
|
|
|
|
it('clears all cache', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('key1'), 'value1'),
|
|
CacheItem::forSet(CacheKey::fromString('key2'), 'value2')
|
|
);
|
|
|
|
$success = $this->cache->clear();
|
|
|
|
expect($success)->toBeTrue();
|
|
expect($this->cache->get(CacheKey::fromString('key1'))->isHit)->toBeFalse();
|
|
expect($this->cache->get(CacheKey::fromString('key2'))->isHit)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Remember Pattern', function () {
|
|
it('executes callback on cache miss', function () {
|
|
$key = CacheKey::fromString('remember-key');
|
|
$callbackExecuted = false;
|
|
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'computed-value';
|
|
};
|
|
|
|
$result = $this->cache->remember($key, $callback, Duration::fromMinutes(5));
|
|
|
|
expect($callbackExecuted)->toBeTrue();
|
|
expect($result->value)->toBe('computed-value');
|
|
});
|
|
|
|
it('returns cached value without executing callback on hit', function () {
|
|
$key = CacheKey::fromString('cached-key');
|
|
|
|
// Set value first
|
|
$this->cache->set(CacheItem::forSet($key, 'cached-value'));
|
|
|
|
$callbackExecuted = false;
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'should-not-execute';
|
|
};
|
|
|
|
$result = $this->cache->remember($key, $callback);
|
|
|
|
expect($callbackExecuted)->toBeFalse();
|
|
expect($result->value)->toBe('cached-value');
|
|
});
|
|
});
|
|
|
|
describe('Prefix Operations', function () {
|
|
it('gets items by prefix', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('user:1'), 'User 1'),
|
|
CacheItem::forSet(CacheKey::fromString('user:2'), 'User 2'),
|
|
CacheItem::forSet(CacheKey::fromString('post:1'), 'Post 1')
|
|
);
|
|
|
|
// Verify driver supports scanning
|
|
$driver = $this->cache->getDriver();
|
|
expect($driver)->toBeInstanceOf(\App\Framework\Cache\Contracts\Scannable::class);
|
|
|
|
$prefix = CachePrefix::fromString('user:');
|
|
$result = $this->cache->get($prefix);
|
|
|
|
// SmartCache uses scanPrefix on the underlying driver
|
|
expect($result->count())->toBe(2);
|
|
expect($result->getValue(CacheKey::fromString('user:1')))->toBe('User 1');
|
|
expect($result->getValue(CacheKey::fromString('user:2')))->toBe('User 2');
|
|
});
|
|
|
|
it('checks existence by prefix', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('api:v1:users'), 'data'),
|
|
CacheItem::forSet(CacheKey::fromString('api:v1:posts'), 'data')
|
|
);
|
|
|
|
$prefix = CachePrefix::fromString('api:v1:');
|
|
$results = $this->cache->has($prefix);
|
|
|
|
expect($results)->toHaveKey('api:v1:users');
|
|
expect($results)->toHaveKey('api:v1:posts');
|
|
expect($results['api:v1:users'])->toBeTrue();
|
|
});
|
|
|
|
it('forgets items by prefix', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('temp:1'), 'data1'),
|
|
CacheItem::forSet(CacheKey::fromString('temp:2'), 'data2'),
|
|
CacheItem::forSet(CacheKey::fromString('perm:1'), 'data3')
|
|
);
|
|
|
|
$prefix = CachePrefix::fromString('temp:');
|
|
$this->cache->forget($prefix);
|
|
|
|
expect($this->cache->get(CacheKey::fromString('temp:1'))->isHit)->toBeFalse();
|
|
expect($this->cache->get(CacheKey::fromString('temp:2'))->isHit)->toBeFalse();
|
|
expect($this->cache->get(CacheKey::fromString('perm:1'))->isHit)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Value Objects', function () {
|
|
it('caches arrays correctly', function () {
|
|
$key = CacheKey::fromNamespace('user', '123');
|
|
$value = ['id' => 123, 'name' => 'Test User', 'active' => true];
|
|
|
|
$this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBeArray();
|
|
expect($result->value['id'])->toBe(123);
|
|
expect($result->value['name'])->toBe('Test User');
|
|
});
|
|
|
|
it('handles complex nested arrays', function () {
|
|
$key = CacheKey::forClass('TestClass', 'method');
|
|
$value = [
|
|
'id' => 1,
|
|
'name' => 'Test',
|
|
'metadata' => [
|
|
'created' => '2024-01-01',
|
|
'updated' => '2024-01-02',
|
|
'tags' => ['php', 'cache', 'testing']
|
|
]
|
|
];
|
|
|
|
$this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value['id'])->toBe(1);
|
|
expect($result->value['metadata'])->toBeArray();
|
|
expect($result->value['metadata']['tags'])->toHaveCount(3);
|
|
});
|
|
});
|
|
|
|
describe('Stats and Monitoring', function () {
|
|
it('provides cache statistics', function () {
|
|
$stats = $this->cache->getStats();
|
|
|
|
expect($stats)->toBeArray();
|
|
expect($stats)->toHaveKey('cache_type');
|
|
expect($stats['cache_type'])->toBe('SmartCache');
|
|
});
|
|
|
|
it('reports async capabilities', function () {
|
|
$stats = $this->cache->getStats();
|
|
|
|
expect($stats)->toHaveKey('async_enabled');
|
|
expect($stats)->toHaveKey('async_available');
|
|
expect($stats)->toHaveKey('async_threshold');
|
|
});
|
|
|
|
it('reports pattern and prefix support', function () {
|
|
$stats = $this->cache->getStats();
|
|
|
|
expect($stats)->toHaveKey('pattern_support');
|
|
expect($stats)->toHaveKey('prefix_support');
|
|
expect($stats['pattern_support'])->toBeTrue();
|
|
expect($stats['prefix_support'])->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', function () {
|
|
it('handles empty operations gracefully', function () {
|
|
expect($this->cache->set())->toBeTrue();
|
|
expect($this->cache->forget())->toBeTrue();
|
|
expect($this->cache->has())->toBe([]);
|
|
expect($this->cache->get())->toBeInstanceOf(CacheResult::class);
|
|
});
|
|
|
|
it('handles null values correctly', function () {
|
|
$key = CacheKey::fromString('null-value');
|
|
|
|
$this->cache->set(CacheItem::forSet($key, null));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBeNull();
|
|
});
|
|
|
|
it('handles false values correctly', function () {
|
|
$key = CacheKey::fromString('false-value');
|
|
|
|
$this->cache->set(CacheItem::forSet($key, false));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBeFalse();
|
|
});
|
|
|
|
it('handles empty string values', function () {
|
|
$key = CacheKey::fromString('empty-string');
|
|
|
|
$this->cache->set(CacheItem::forSet($key, ''));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
// Empty strings may be returned as null after serialization/deserialization
|
|
expect($result->value === '' || $result->value === null)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('CacheResult Features', function () {
|
|
it('provides hit and miss separation', function () {
|
|
$this->cache->set(CacheItem::forSet(CacheKey::fromString('exists'), 'value'));
|
|
|
|
$result = $this->cache->get(
|
|
CacheKey::fromString('exists'),
|
|
CacheKey::fromString('not-exists')
|
|
);
|
|
|
|
expect($result->count())->toBe(2);
|
|
expect($result->getHits()->count())->toBe(1);
|
|
expect($result->getMisses()->count())->toBe(1);
|
|
});
|
|
|
|
it('calculates hit ratio', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('hit1'), 'value1'),
|
|
CacheItem::forSet(CacheKey::fromString('hit2'), 'value2')
|
|
);
|
|
|
|
$result = $this->cache->get(
|
|
CacheKey::fromString('hit1'),
|
|
CacheKey::fromString('hit2'),
|
|
CacheKey::fromString('miss1'),
|
|
CacheKey::fromString('miss2')
|
|
);
|
|
|
|
expect($result->getHitRatio())->toBe(0.5);
|
|
});
|
|
|
|
it('checks for complete hits', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('key1'), 'value1'),
|
|
CacheItem::forSet(CacheKey::fromString('key2'), 'value2')
|
|
);
|
|
|
|
$result = $this->cache->get(
|
|
CacheKey::fromString('key1'),
|
|
CacheKey::fromString('key2')
|
|
);
|
|
|
|
expect($result->isCompleteHit())->toBeTrue();
|
|
expect($result->isCompleteMiss())->toBeFalse();
|
|
});
|
|
|
|
it('checks for complete misses', function () {
|
|
$result = $this->cache->get(
|
|
CacheKey::fromString('miss1'),
|
|
CacheKey::fromString('miss2')
|
|
);
|
|
|
|
expect($result->isCompleteMiss())->toBeTrue();
|
|
expect($result->isCompleteHit())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Wrapped Cache Access', function () {
|
|
it('provides access to wrapped cache', function () {
|
|
$wrapped = $this->cache->getWrappedCache();
|
|
|
|
expect($wrapped)->toBeInstanceOf(GeneralCache::class);
|
|
});
|
|
|
|
it('provides access to underlying driver', function () {
|
|
$driver = $this->cache->getDriver();
|
|
|
|
expect($driver)->toBeInstanceOf(InMemoryCache::class);
|
|
});
|
|
|
|
it('checks driver interface support', function () {
|
|
$supportsDriver = $this->cache->driverSupports(\App\Framework\Cache\CacheDriver::class);
|
|
|
|
expect($supportsDriver)->toBeTrue();
|
|
});
|
|
});
|
|
});
|