Files
michaelschiemer/tests/Unit/Framework/Redis/RedisKvStoreTest.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

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();
});
});