- 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.
291 lines
8.8 KiB
PHP
291 lines
8.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Redis\InvalidRedisTtlException;
|
|
use App\Framework\Redis\RedisConfig;
|
|
use App\Framework\Redis\RedisConnection;
|
|
use App\Framework\Redis\RedisKvStore;
|
|
use App\Framework\Serializer\Json\JsonSerializer;
|
|
|
|
beforeEach(function () {
|
|
// Create Redis connection for testing
|
|
$config = new RedisConfig(
|
|
host: 'redis',
|
|
port: 6379,
|
|
database: 15 // Use separate database for tests
|
|
);
|
|
|
|
$this->connection = new RedisConnection($config, 'test');
|
|
$this->serializer = new JsonSerializer();
|
|
$this->store = new RedisKvStore($this->connection, $this->serializer);
|
|
|
|
// Clean up before each test
|
|
$this->connection->flush();
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Clean up after each test
|
|
$this->connection->flush();
|
|
});
|
|
|
|
describe('Basic Operations', function () {
|
|
it('can set and get string values', function () {
|
|
$result = $this->store->set('test-key', 'test-value');
|
|
|
|
expect($result)->toBeTrue();
|
|
expect($this->store->get('test-key'))->toBe('test-value');
|
|
});
|
|
|
|
it('returns null for non-existent keys', function () {
|
|
expect($this->store->get('non-existent'))->toBeNull();
|
|
});
|
|
|
|
it('can delete keys', function () {
|
|
$this->store->set('key1', 'value1');
|
|
$this->store->set('key2', 'value2');
|
|
|
|
$deleted = $this->store->delete('key1', 'key2');
|
|
|
|
expect($deleted)->toBe(2);
|
|
expect($this->store->get('key1'))->toBeNull();
|
|
expect($this->store->get('key2'))->toBeNull();
|
|
});
|
|
|
|
it('can check if keys exist', function () {
|
|
$this->store->set('existing', 'value');
|
|
|
|
expect($this->store->exists('existing'))->toBe(1);
|
|
expect($this->store->exists('non-existent'))->toBe(0);
|
|
expect($this->store->exists('existing', 'non-existent'))->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('JSON Serialization', function () {
|
|
it('automatically serializes arrays to JSON', function () {
|
|
$data = ['name' => 'John', 'age' => 30, 'active' => true];
|
|
|
|
$this->store->set('user', $data);
|
|
$retrieved = $this->store->get('user');
|
|
|
|
expect($retrieved)->toBe($data);
|
|
});
|
|
|
|
it('handles nested arrays', function () {
|
|
$data = [
|
|
'user' => ['name' => 'John', 'email' => 'john@example.com'],
|
|
'settings' => ['theme' => 'dark', 'notifications' => true],
|
|
];
|
|
|
|
$this->store->set('complex', $data);
|
|
|
|
expect($this->store->get('complex'))->toBe($data);
|
|
});
|
|
|
|
it('handles numeric values', function () {
|
|
$this->store->set('integer', 42);
|
|
$this->store->set('float', 3.14);
|
|
|
|
expect($this->store->get('integer'))->toBe('42');
|
|
expect($this->store->get('float'))->toBe('3.14');
|
|
});
|
|
|
|
it('handles boolean values', function () {
|
|
$this->store->set('true-val', true);
|
|
$this->store->set('false-val', false);
|
|
|
|
expect($this->store->get('true-val'))->toBe('1');
|
|
expect($this->store->get('false-val'))->toBe('');
|
|
});
|
|
});
|
|
|
|
describe('TTL Support', function () {
|
|
it('can set expiration with Duration', function () {
|
|
$this->store->set('expiring-key', 'value', Duration::fromSeconds(2));
|
|
|
|
expect($this->store->get('expiring-key'))->toBe('value');
|
|
|
|
// Check TTL is set
|
|
$ttl = $this->store->ttl('expiring-key');
|
|
expect($ttl)->toBeGreaterThan(0)->toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it('can set expiration with DateTimeInterface', function () {
|
|
$expiration = new DateTimeImmutable('+5 seconds');
|
|
$this->store->set('expiring-key', 'value', $expiration);
|
|
|
|
$ttl = $this->store->ttl('expiring-key');
|
|
expect($ttl)->toBeGreaterThan(0)->toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('returns -1 for keys without expiration', function () {
|
|
$this->store->set('permanent-key', 'value');
|
|
|
|
expect($this->store->ttl('permanent-key'))->toBe(-1);
|
|
});
|
|
|
|
it('returns -2 for non-existent keys', function () {
|
|
expect($this->store->ttl('non-existent'))->toBe(-2);
|
|
});
|
|
|
|
it('can set expiration on existing key', function () {
|
|
$this->store->set('key', 'value');
|
|
|
|
$result = $this->store->expire('key', Duration::fromSeconds(10));
|
|
|
|
expect($result)->toBeTrue();
|
|
expect($this->store->ttl('key'))->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('throws exception for negative TTL', function () {
|
|
expect(fn () => $this->store->set('key', 'value', Duration::fromSeconds(-10)))
|
|
->toThrow(InvalidRedisTtlException::class);
|
|
});
|
|
|
|
it('throws exception for zero TTL', function () {
|
|
expect(fn () => $this->store->set('key', 'value', Duration::fromSeconds(0)))
|
|
->toThrow(InvalidRedisTtlException::class);
|
|
});
|
|
});
|
|
|
|
describe('Increment/Decrement', function () {
|
|
it('can increment numeric values', function () {
|
|
$this->store->set('counter', '0');
|
|
|
|
$newValue = $this->store->increment('counter');
|
|
expect($newValue)->toBe(1);
|
|
|
|
$newValue = $this->store->increment('counter', 5);
|
|
expect($newValue)->toBe(6);
|
|
});
|
|
|
|
it('can decrement numeric values', function () {
|
|
$this->store->set('counter', '10');
|
|
|
|
$newValue = $this->store->decrement('counter');
|
|
expect($newValue)->toBe(9);
|
|
|
|
$newValue = $this->store->decrement('counter', 3);
|
|
expect($newValue)->toBe(6);
|
|
});
|
|
|
|
it('initializes non-existent keys to 0 before incrementing', function () {
|
|
$value = $this->store->increment('new-counter');
|
|
|
|
expect($value)->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Multiple Operations', function () {
|
|
it('can get multiple values at once', function () {
|
|
$this->store->set('key1', 'value1');
|
|
$this->store->set('key2', 'value2');
|
|
$this->store->set('key3', 'value3');
|
|
|
|
$values = $this->store->getMultiple('key1', 'key2', 'non-existent');
|
|
|
|
expect($values)->toBe([
|
|
'key1' => 'value1',
|
|
'key2' => 'value2',
|
|
'non-existent' => null,
|
|
]);
|
|
});
|
|
|
|
it('can set multiple values at once', function () {
|
|
$result = $this->store->setMultiple([
|
|
'user:1' => 'John',
|
|
'user:2' => 'Jane',
|
|
'user:3' => 'Bob',
|
|
]);
|
|
|
|
expect($result)->toBeTrue();
|
|
expect($this->store->get('user:1'))->toBe('John');
|
|
expect($this->store->get('user:2'))->toBe('Jane');
|
|
expect($this->store->get('user:3'))->toBe('Bob');
|
|
});
|
|
|
|
it('serializes complex data in multiple operations', function () {
|
|
$this->store->setMultiple([
|
|
'data1' => ['name' => 'John'],
|
|
'data2' => ['name' => 'Jane'],
|
|
]);
|
|
|
|
$values = $this->store->getMultiple('data1', 'data2');
|
|
|
|
expect($values['data1'])->toBe(['name' => 'John']);
|
|
expect($values['data2'])->toBe(['name' => 'Jane']);
|
|
});
|
|
});
|
|
|
|
describe('Key Prefix', function () {
|
|
it('applies prefix to all keys', function () {
|
|
$storeWithPrefix = new RedisKvStore(
|
|
$this->connection,
|
|
$this->serializer,
|
|
'test:'
|
|
);
|
|
|
|
$storeWithPrefix->set('mykey', 'myvalue');
|
|
|
|
// Should not be accessible without prefix
|
|
expect($this->store->get('mykey'))->toBeNull();
|
|
|
|
// Should be accessible with prefix
|
|
expect($storeWithPrefix->get('mykey'))->toBe('myvalue');
|
|
|
|
// Verify actual Redis key has prefix
|
|
expect($this->connection->get('test:mykey'))->toBe('myvalue');
|
|
});
|
|
|
|
it('applies prefix to delete operations', function () {
|
|
$storeWithPrefix = new RedisKvStore(
|
|
$this->connection,
|
|
$this->serializer,
|
|
'app:'
|
|
);
|
|
|
|
$storeWithPrefix->set('key1', 'value1');
|
|
$storeWithPrefix->set('key2', 'value2');
|
|
|
|
$deleted = $storeWithPrefix->delete('key1', 'key2');
|
|
|
|
expect($deleted)->toBe(2);
|
|
expect($this->connection->exists('app:key1', 'app:key2'))->toBe(0);
|
|
});
|
|
|
|
it('applies prefix to multiple operations', function () {
|
|
$storeWithPrefix = new RedisKvStore(
|
|
$this->connection,
|
|
$this->serializer,
|
|
'cache:'
|
|
);
|
|
|
|
$storeWithPrefix->setMultiple([
|
|
'user:1' => 'John',
|
|
'user:2' => 'Jane',
|
|
]);
|
|
|
|
$values = $storeWithPrefix->getMultiple('user:1', 'user:2');
|
|
|
|
expect($values)->toBe([
|
|
'user:1' => 'John',
|
|
'user:2' => 'Jane',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Flush Operation', function () {
|
|
it('can flush all keys in database', function () {
|
|
$this->store->set('key1', 'value1');
|
|
$this->store->set('key2', 'value2');
|
|
$this->store->set('key3', 'value3');
|
|
|
|
$this->store->flush();
|
|
|
|
expect($this->store->get('key1'))->toBeNull();
|
|
expect($this->store->get('key2'))->toBeNull();
|
|
expect($this->store->get('key3'))->toBeNull();
|
|
});
|
|
});
|