feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Database\EntityManager;
|
||||
use App\Framework\LiveComponents\Attributes\TrackStateHistory;
|
||||
use App\Framework\StateManagement\Database\DatabaseStateHistoryManager;
|
||||
use App\Framework\StateManagement\Database\StateChangeType;
|
||||
use App\Framework\StateManagement\Database\StateHistoryEntry;
|
||||
|
||||
// Test Component with TrackStateHistory
|
||||
#[TrackStateHistory(
|
||||
trackIpAddress: true,
|
||||
trackUserAgent: true,
|
||||
trackChangedProperties: true
|
||||
)]
|
||||
final readonly class TestHistoryComponent
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
|
||||
// Test Component without TrackStateHistory
|
||||
final readonly class TestNoHistoryComponent
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
|
||||
describe('DatabaseStateHistoryManager', 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;
|
||||
|
||||
// Create DatabaseStateHistoryManager
|
||||
$this->historyManager = new DatabaseStateHistoryManager(
|
||||
entityManager: $this->entityManager,
|
||||
logger: null
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
describe('addHistoryEntry', function () {
|
||||
it('adds history entry to database', function () {
|
||||
$componentId = 'test-component:1';
|
||||
$stateData = json_encode(['count' => 42, 'message' => 'test']);
|
||||
$stateClass = 'TestComponentState';
|
||||
$version = 1;
|
||||
$changeType = StateChangeType::CREATED->value;
|
||||
$context = [
|
||||
'user_id' => 'user-123',
|
||||
'session_id' => 'session-456',
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0'
|
||||
];
|
||||
$changedProperties = ['count', 'message'];
|
||||
$currentChecksum = hash('sha256', $stateData);
|
||||
|
||||
// Expect persist and commit
|
||||
$this->unitOfWork
|
||||
->shouldReceive('persist')
|
||||
->once()
|
||||
->with(Mockery::type(StateHistoryEntry::class));
|
||||
|
||||
$this->unitOfWork
|
||||
->shouldReceive('commit')
|
||||
->once();
|
||||
|
||||
// Add history entry
|
||||
$this->historyManager->addHistoryEntry(
|
||||
componentId: $componentId,
|
||||
stateData: $stateData,
|
||||
stateClass: $stateClass,
|
||||
version: $version,
|
||||
changeType: $changeType,
|
||||
context: $context,
|
||||
changedProperties: $changedProperties,
|
||||
previousChecksum: null,
|
||||
currentChecksum: $currentChecksum
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistory', function () {
|
||||
it('returns history entries ordered by created_at DESC', function () {
|
||||
$componentId = 'test-component:2';
|
||||
|
||||
// Mock: History entries
|
||||
$entries = [
|
||||
new StateHistoryEntry(
|
||||
id: 2,
|
||||
componentId: $componentId,
|
||||
stateData: json_encode(['count' => 10]),
|
||||
stateClass: 'TestState',
|
||||
version: 2,
|
||||
changeType: StateChangeType::UPDATED,
|
||||
changedProperties: ['count'],
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: 'checksum1',
|
||||
currentChecksum: 'checksum2',
|
||||
createdAt: Timestamp::now()
|
||||
),
|
||||
new StateHistoryEntry(
|
||||
id: 1,
|
||||
componentId: $componentId,
|
||||
stateData: json_encode(['count' => 0]),
|
||||
stateClass: 'TestState',
|
||||
version: 1,
|
||||
changeType: StateChangeType::CREATED,
|
||||
changedProperties: null,
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: null,
|
||||
currentChecksum: 'checksum1',
|
||||
createdAt: Timestamp::now()
|
||||
),
|
||||
];
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(
|
||||
StateHistoryEntry::class,
|
||||
['component_id' => $componentId],
|
||||
['created_at' => 'DESC'],
|
||||
100,
|
||||
0
|
||||
)
|
||||
->andReturn($entries);
|
||||
|
||||
// Get history
|
||||
$result = $this->historyManager->getHistory($componentId, limit: 100);
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result)->toHaveCount(2);
|
||||
expect($result[0]->version)->toBe(2);
|
||||
expect($result[1]->version)->toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoryByVersion', function () {
|
||||
it('returns specific version from history', function () {
|
||||
$componentId = 'test-component:3';
|
||||
$version = 5;
|
||||
|
||||
// Mock: History entry
|
||||
$entry = new StateHistoryEntry(
|
||||
id: 5,
|
||||
componentId: $componentId,
|
||||
stateData: json_encode(['count' => 50]),
|
||||
stateClass: 'TestState',
|
||||
version: $version,
|
||||
changeType: StateChangeType::UPDATED,
|
||||
changedProperties: ['count'],
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: 'checksum4',
|
||||
currentChecksum: 'checksum5',
|
||||
createdAt: Timestamp::now()
|
||||
);
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(
|
||||
StateHistoryEntry::class,
|
||||
[
|
||||
'component_id' => $componentId,
|
||||
'version' => $version
|
||||
],
|
||||
Mockery::any(),
|
||||
1
|
||||
)
|
||||
->andReturn([$entry]);
|
||||
|
||||
// Get specific version
|
||||
$result = $this->historyManager->getHistoryByVersion($componentId, $version);
|
||||
|
||||
expect($result)->toBeInstanceOf(StateHistoryEntry::class);
|
||||
expect($result->version)->toBe(5);
|
||||
expect($result->componentId)->toBe($componentId);
|
||||
});
|
||||
|
||||
it('returns null when version does not exist', function () {
|
||||
$componentId = 'test-component:4';
|
||||
$version = 999;
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->andReturn([]);
|
||||
|
||||
// Get non-existent version
|
||||
$result = $this->historyManager->getHistoryByVersion($componentId, $version);
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoryByUser', function () {
|
||||
it('returns history entries for specific user', function () {
|
||||
$userId = 'user-123';
|
||||
|
||||
// Mock: User's history entries
|
||||
$entries = [
|
||||
new StateHistoryEntry(
|
||||
id: 1,
|
||||
componentId: 'comp-1',
|
||||
stateData: json_encode(['data' => 'test']),
|
||||
stateClass: 'TestState',
|
||||
version: 1,
|
||||
changeType: StateChangeType::CREATED,
|
||||
changedProperties: null,
|
||||
userId: $userId,
|
||||
sessionId: 'session-1',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: null,
|
||||
currentChecksum: 'checksum1',
|
||||
createdAt: Timestamp::now()
|
||||
),
|
||||
];
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(
|
||||
StateHistoryEntry::class,
|
||||
['user_id' => $userId],
|
||||
['created_at' => 'DESC'],
|
||||
100
|
||||
)
|
||||
->andReturn($entries);
|
||||
|
||||
// Get user history
|
||||
$result = $this->historyManager->getHistoryByUser($userId);
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result)->toHaveCount(1);
|
||||
expect($result[0]->userId)->toBe($userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', function () {
|
||||
it('deletes old entries keeping only last N', function () {
|
||||
$componentId = 'test-component:5';
|
||||
$keepLast = 2;
|
||||
|
||||
// Mock: 5 entries, we keep 2, delete 3
|
||||
$entries = array_map(
|
||||
fn(int $i) => new StateHistoryEntry(
|
||||
id: $i,
|
||||
componentId: $componentId,
|
||||
stateData: json_encode(['version' => $i]),
|
||||
stateClass: 'TestState',
|
||||
version: $i,
|
||||
changeType: StateChangeType::UPDATED,
|
||||
changedProperties: null,
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: "checksum{$i}",
|
||||
currentChecksum: "checksum" . ($i + 1),
|
||||
createdAt: Timestamp::now()
|
||||
),
|
||||
range(5, 1, -1) // DESC order
|
||||
);
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(
|
||||
StateHistoryEntry::class,
|
||||
['component_id' => $componentId],
|
||||
['created_at' => 'DESC']
|
||||
)
|
||||
->andReturn($entries);
|
||||
|
||||
// Expect remove for 3 oldest entries
|
||||
$this->unitOfWork
|
||||
->shouldReceive('remove')
|
||||
->times(3)
|
||||
->with(Mockery::type(StateHistoryEntry::class));
|
||||
|
||||
$this->unitOfWork
|
||||
->shouldReceive('commit')
|
||||
->once();
|
||||
|
||||
// Cleanup
|
||||
$deletedCount = $this->historyManager->cleanup($componentId, $keepLast);
|
||||
|
||||
expect($deletedCount)->toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteHistory', function () {
|
||||
it('deletes all history for component', function () {
|
||||
$componentId = 'test-component:6';
|
||||
|
||||
// Mock: 3 entries to delete
|
||||
$entries = array_map(
|
||||
fn(int $i) => new StateHistoryEntry(
|
||||
id: $i,
|
||||
componentId: $componentId,
|
||||
stateData: json_encode(['data' => 'test']),
|
||||
stateClass: 'TestState',
|
||||
version: $i,
|
||||
changeType: StateChangeType::UPDATED,
|
||||
changedProperties: null,
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: "checksum{$i}",
|
||||
currentChecksum: "checksum" . ($i + 1),
|
||||
createdAt: Timestamp::now()
|
||||
),
|
||||
range(1, 3)
|
||||
);
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(
|
||||
StateHistoryEntry::class,
|
||||
['component_id' => $componentId]
|
||||
)
|
||||
->andReturn($entries);
|
||||
|
||||
// Expect remove for all entries
|
||||
$this->unitOfWork
|
||||
->shouldReceive('remove')
|
||||
->times(3)
|
||||
->with(Mockery::type(StateHistoryEntry::class));
|
||||
|
||||
$this->unitOfWork
|
||||
->shouldReceive('commit')
|
||||
->once();
|
||||
|
||||
// Delete history
|
||||
$deletedCount = $this->historyManager->deleteHistory($componentId);
|
||||
|
||||
expect($deletedCount)->toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHistoryEnabled', function () {
|
||||
it('returns true when component has TrackStateHistory attribute', function () {
|
||||
$result = $this->historyManager->isHistoryEnabled(TestHistoryComponent::class);
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false when component does not have TrackStateHistory attribute', function () {
|
||||
$result = $this->historyManager->isHistoryEnabled(TestNoHistoryComponent::class);
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false when class does not exist', function () {
|
||||
$result = $this->historyManager->isHistoryEnabled('NonExistentClass');
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', function () {
|
||||
it('returns statistics about history storage', function () {
|
||||
// Mock: Multiple entries
|
||||
$entries = [
|
||||
new StateHistoryEntry(
|
||||
id: 1,
|
||||
componentId: 'comp-1',
|
||||
stateData: json_encode(['data' => 'test']),
|
||||
stateClass: 'TestState',
|
||||
version: 1,
|
||||
changeType: StateChangeType::CREATED,
|
||||
changedProperties: null,
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: null,
|
||||
currentChecksum: 'checksum1',
|
||||
createdAt: Timestamp::fromString('2024-01-01 10:00:00')
|
||||
),
|
||||
new StateHistoryEntry(
|
||||
id: 2,
|
||||
componentId: 'comp-2',
|
||||
stateData: json_encode(['data' => 'test']),
|
||||
stateClass: 'TestState',
|
||||
version: 1,
|
||||
changeType: StateChangeType::CREATED,
|
||||
changedProperties: null,
|
||||
userId: 'user-456',
|
||||
sessionId: 'session-789',
|
||||
ipAddress: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
previousChecksum: null,
|
||||
currentChecksum: 'checksum2',
|
||||
createdAt: Timestamp::fromString('2024-01-02 10:00:00')
|
||||
),
|
||||
];
|
||||
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(StateHistoryEntry::class, [])
|
||||
->andReturn($entries);
|
||||
|
||||
// Get statistics
|
||||
$stats = $this->historyManager->getStatistics();
|
||||
|
||||
expect($stats)->toBeArray();
|
||||
expect($stats['total_entries'])->toBe(2);
|
||||
expect($stats['total_components'])->toBe(2);
|
||||
expect($stats['oldest_entry'])->toBeInstanceOf(Timestamp::class);
|
||||
expect($stats['newest_entry'])->toBeInstanceOf(Timestamp::class);
|
||||
});
|
||||
|
||||
it('returns empty statistics when no entries exist', function () {
|
||||
$this->entityManager
|
||||
->shouldReceive('findBy')
|
||||
->once()
|
||||
->with(StateHistoryEntry::class, [])
|
||||
->andReturn([]);
|
||||
|
||||
// Get statistics
|
||||
$stats = $this->historyManager->getStatistics();
|
||||
|
||||
expect($stats)->toBeArray();
|
||||
expect($stats['total_entries'])->toBe(0);
|
||||
expect($stats['total_components'])->toBe(0);
|
||||
expect($stats['oldest_entry'])->toBeNull();
|
||||
expect($stats['newest_entry'])->toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Cache\SmartCache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Database\EntityManager;
|
||||
use App\Framework\StateManagement\Database\ComponentStateEntity;
|
||||
use App\Framework\StateManagement\Database\DatabaseStateManager;
|
||||
use App\Framework\StateManagement\SerializableState;
|
||||
|
||||
// Test State Value Object
|
||||
final readonly class TestComponentState implements SerializableState
|
||||
{
|
||||
public function __construct(
|
||||
public int $count = 0,
|
||||
public string $message = 'test'
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user