$this->count, 'message' => $this->message, ]; } public static function fromArray(array $data): self { return new self( count: $data['count'] ?? 0, message: $data['message'] ?? 'test' ); } } describe('DatabaseStateManager', function () { beforeEach(function () { // Mock EntityManager $this->entityManager = Mockery::mock(EntityManager::class); $this->unitOfWork = Mockery::mock(\App\Framework\Database\UnitOfWork::class); $this->entityManager->unitOfWork = $this->unitOfWork; // Real Cache for testing $this->cache = new SmartCache(); // Create DatabaseStateManager $this->stateManager = new DatabaseStateManager( entityManager: $this->entityManager, cache: $this->cache, stateClass: TestComponentState::class, logger: null, cacheTtl: Duration::fromSeconds(60) ); }); afterEach(function () { Mockery::close(); }); describe('setState', function () { it('creates new state entity when none exists', function () { $key = 'test-component:1'; $state = new TestComponentState(count: 42, message: 'hello'); // Mock: Entity does not exist $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn(null); // Expect persist and commit $this->unitOfWork ->shouldReceive('persist') ->once() ->with(Mockery::type(ComponentStateEntity::class)); $this->unitOfWork ->shouldReceive('commit') ->once(); // Set state $this->stateManager->setState($key, $state); // Verify cache was populated $cached = $this->cache->get(CacheKey::from("component_state:{$key}")); expect($cached->isHit)->toBeTrue(); }); it('updates existing state entity', function () { $key = 'test-component:2'; $state = new TestComponentState(count: 100, message: 'updated'); // Mock: Entity exists $existingEntity = new ComponentStateEntity( componentId: $key, stateData: json_encode(['count' => 50, 'message' => 'old']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'old-checksum', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ); $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($existingEntity); // Expect persist with updated entity $this->unitOfWork ->shouldReceive('persist') ->once() ->with(Mockery::on(function (ComponentStateEntity $entity) { return $entity->version === 2; })); $this->unitOfWork ->shouldReceive('commit') ->once(); // Set state $this->stateManager->setState($key, $state); }); }); describe('getState', function () { it('returns state from cache on hit', function () { $key = 'test-component:3'; $state = new TestComponentState(count: 77, message: 'cached'); // Populate cache $stateData = json_encode($state->toArray()); $this->cache->set( CacheKey::from("component_state:{$key}"), $stateData, Duration::fromSeconds(60) ); // Should NOT call EntityManager $this->entityManager->shouldNotReceive('find'); // Get state $result = $this->stateManager->getState($key); expect($result)->toBeInstanceOf(TestComponentState::class); expect($result->count)->toBe(77); expect($result->message)->toBe('cached'); }); it('falls back to database on cache miss', function () { $key = 'test-component:4'; $state = new TestComponentState(count: 88, message: 'database'); // Mock: Entity exists in database $entity = new ComponentStateEntity( componentId: $key, stateData: json_encode($state->toArray()), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: hash('sha256', json_encode($state->toArray())), createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ); $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($entity); // Get state $result = $this->stateManager->getState($key); expect($result)->toBeInstanceOf(TestComponentState::class); expect($result->count)->toBe(88); expect($result->message)->toBe('database'); // Cache should be populated $cached = $this->cache->get(CacheKey::from("component_state:{$key}")); expect($cached->isHit)->toBeTrue(); }); it('returns null when state does not exist', function () { $key = 'test-component:nonexistent'; // Mock: Entity does not exist $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn(null); // Get state $result = $this->stateManager->getState($key); expect($result)->toBeNull(); }); }); describe('hasState', function () { it('returns true when state exists in cache', function () { $key = 'test-component:5'; $state = new TestComponentState(count: 99, message: 'exists'); // Populate cache $this->cache->set( CacheKey::from("component_state:{$key}"), json_encode($state->toArray()), Duration::fromSeconds(60) ); // Should NOT call EntityManager $this->entityManager->shouldNotReceive('find'); // Check existence $result = $this->stateManager->hasState($key); expect($result)->toBeTrue(); }); it('checks database when not in cache', function () { $key = 'test-component:6'; // Mock: Entity exists in database $entity = new ComponentStateEntity( componentId: $key, stateData: json_encode(['count' => 1, 'message' => 'test']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'checksum', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ); $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($entity); // Check existence $result = $this->stateManager->hasState($key); expect($result)->toBeTrue(); }); it('returns false when state does not exist', function () { $key = 'test-component:nonexistent'; // Mock: Entity does not exist $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn(null); // Check existence $result = $this->stateManager->hasState($key); expect($result)->toBeFalse(); }); }); describe('removeState', function () { it('removes state from database and cache', function () { $key = 'test-component:7'; // Populate cache $this->cache->set( CacheKey::from("component_state:{$key}"), json_encode(['count' => 1, 'message' => 'test']), Duration::fromSeconds(60) ); // Mock: Entity exists $entity = new ComponentStateEntity( componentId: $key, stateData: json_encode(['count' => 1, 'message' => 'test']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'checksum', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ); $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($entity); // Expect remove and commit $this->unitOfWork ->shouldReceive('remove') ->once() ->with($entity); $this->unitOfWork ->shouldReceive('commit') ->once(); // Remove state $this->stateManager->removeState($key); // Cache should be cleared $cached = $this->cache->get(CacheKey::from("component_state:{$key}")); expect($cached->isHit)->toBeFalse(); }); }); describe('updateState', function () { it('atomically updates state with updater function', function () { $key = 'test-component:8'; $initialState = new TestComponentState(count: 10, message: 'initial'); // Mock: Get current state $entity = new ComponentStateEntity( componentId: $key, stateData: json_encode($initialState->toArray()), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: hash('sha256', json_encode($initialState->toArray())), createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ); $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($entity); // Expect persist with updated state $this->entityManager ->shouldReceive('find') ->once() ->with(ComponentStateEntity::class, $key) ->andReturn($entity); $this->unitOfWork ->shouldReceive('persist') ->once() ->with(Mockery::type(ComponentStateEntity::class)); $this->unitOfWork ->shouldReceive('commit') ->once(); // Update state $result = $this->stateManager->updateState( $key, fn(TestComponentState $state) => new TestComponentState( count: $state->count + 5, message: 'updated' ) ); expect($result)->toBeInstanceOf(TestComponentState::class); expect($result->count)->toBe(15); expect($result->message)->toBe('updated'); }); }); describe('getAllStates', function () { it('returns all states for state class', function () { // Mock: Multiple entities $entities = [ new ComponentStateEntity( componentId: 'test-component:1', stateData: json_encode(['count' => 1, 'message' => 'first']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'checksum1', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ), new ComponentStateEntity( componentId: 'test-component:2', stateData: json_encode(['count' => 2, 'message' => 'second']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'checksum2', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ), ]; $this->entityManager ->shouldReceive('findBy') ->once() ->with( ComponentStateEntity::class, ['state_class' => TestComponentState::class] ) ->andReturn($entities); // Get all states $result = $this->stateManager->getAllStates(); expect($result)->toBeArray(); expect($result)->toHaveCount(2); expect($result['test-component:1'])->toBeInstanceOf(TestComponentState::class); expect($result['test-component:1']->count)->toBe(1); expect($result['test-component:2']->count)->toBe(2); }); }); describe('getStatistics', function () { it('returns statistics about state storage', function () { // Mock: Multiple entities $entities = [ new ComponentStateEntity( componentId: 'test-component:1', stateData: json_encode(['count' => 1, 'message' => 'test']), stateClass: TestComponentState::class, componentName: 'test-component', userId: null, sessionId: null, version: 1, checksum: 'checksum', createdAt: Timestamp::now(), updatedAt: Timestamp::now(), expiresAt: null ), ]; $this->entityManager ->shouldReceive('findBy') ->once() ->with( ComponentStateEntity::class, ['state_class' => TestComponentState::class] ) ->andReturn($entities); // Get statistics $stats = $this->stateManager->getStatistics(); expect($stats->totalKeys)->toBe(1); expect($stats->hitCount)->toBeInt(); expect($stats->missCount)->toBeInt(); }); }); });