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