Files
michaelschiemer/tests/Unit/Framework/LiveComponents/Persistence/LiveComponentStatePersistenceTest.php.skip

348 lines
12 KiB
Plaintext

<?php
declare(strict_types=1);
use App\Framework\Http\RequestContext;
use App\Framework\LiveComponents\Attributes\TrackStateHistory;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Persistence\LiveComponentStatePersistence;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\StateManagement\SerializableState;
use App\Framework\StateManagement\StateHistoryManager;
use App\Framework\StateManagement\StateManager;
// Test State
final readonly class TestPersistenceState implements SerializableState
{
public function __construct(
public int $count = 0,
public string $name = 'test'
) {}
public function toArray(): array
{
return [
'count' => $this->count,
'name' => $this->name,
];
}
public static function fromArray(array $data): self
{
return new self(
count: $data['count'] ?? 0,
name: $data['name'] ?? 'test'
);
}
}
// Test Component with History
#[TrackStateHistory(
trackIpAddress: true,
trackUserAgent: true,
trackChangedProperties: true
)]
final readonly class TestTrackedComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public TestPersistenceState $state
) {}
}
// Test Component without History
final readonly class TestUntrackedComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public TestPersistenceState $state
) {}
}
describe('LiveComponentStatePersistence', function () {
beforeEach(function () {
// Mock StateManager
$this->stateManager = Mockery::mock(StateManager::class);
// Mock StateHistoryManager
$this->historyManager = Mockery::mock(StateHistoryManager::class);
// Mock RequestContext
$this->requestContext = new RequestContext(
userId: 'user-123',
sessionId: 'session-456',
ipAddress: '127.0.0.1',
userAgent: 'Mozilla/5.0'
);
// Create persistence handler
$this->persistence = new LiveComponentStatePersistence(
stateManager: $this->stateManager,
historyManager: $this->historyManager,
requestContext: $this->requestContext,
logger: null
);
});
afterEach(function () {
Mockery::close();
});
describe('persistState', function () {
it('persists state without history when component has no TrackStateHistory', function () {
$componentId = new ComponentId('untracked-comp', 'instance-1');
$newState = new TestPersistenceState(count: 42, name: 'updated');
$component = new TestUntrackedComponent($componentId, $newState);
// Mock: Get previous state (none exists)
$this->stateManager
->shouldReceive('getState')
->once()
->with($componentId->toString())
->andReturn(null);
// Expect state to be persisted
$this->stateManager
->shouldReceive('setState')
->once()
->with($componentId->toString(), $newState);
// History tracking should check if enabled (returns false)
$this->historyManager
->shouldReceive('isHistoryEnabled')
->once()
->with(TestUntrackedComponent::class)
->andReturn(false);
// Should NOT add history entry
$this->historyManager
->shouldNotReceive('addHistoryEntry');
// Persist state
$this->persistence->persistState($component, $newState, 'testAction');
});
it('persists state with history when component has TrackStateHistory', function () {
$componentId = new ComponentId('tracked-comp', 'instance-2');
$previousState = new TestPersistenceState(count: 10, name: 'old');
$newState = new TestPersistenceState(count: 42, name: 'new');
$component = new TestTrackedComponent($componentId, $newState);
// Mock: Get previous state
$this->stateManager
->shouldReceive('getState')
->once()
->with($componentId->toString())
->andReturn($previousState);
// Expect state to be persisted
$this->stateManager
->shouldReceive('setState')
->once()
->with($componentId->toString(), $newState);
// History tracking should check if enabled (returns true)
$this->historyManager
->shouldReceive('isHistoryEnabled')
->once()
->with(TestTrackedComponent::class)
->andReturn(true);
// Mock: Get history for version calculation
$this->historyManager
->shouldReceive('getHistory')
->once()
->with($componentId->toString(), Mockery::any())
->andReturn([]);
// Expect history entry to be added
$this->historyManager
->shouldReceive('addHistoryEntry')
->once()
->with(
componentId: $componentId->toString(),
stateData: json_encode($newState->toArray()),
stateClass: TestPersistenceState::class,
version: Mockery::any(),
changeType: 'updated', // Previous state exists
context: Mockery::on(function ($context) {
return isset($context['user_id'])
&& isset($context['session_id'])
&& isset($context['ip_address'])
&& isset($context['user_agent']);
}),
changedProperties: Mockery::on(function ($changed) {
// Both count and name changed
return is_array($changed) && count($changed) === 2;
}),
previousChecksum: Mockery::type('string'),
currentChecksum: Mockery::type('string')
);
// Persist state
$this->persistence->persistState($component, $newState, 'testAction');
});
it('tracks changed properties correctly', function () {
$componentId = new ComponentId('tracked-comp', 'instance-3');
$previousState = new TestPersistenceState(count: 10, name: 'same');
$newState = new TestPersistenceState(count: 42, name: 'same'); // Only count changed
$component = new TestTrackedComponent($componentId, $newState);
// Mock setup
$this->stateManager
->shouldReceive('getState')
->once()
->andReturn($previousState);
$this->stateManager
->shouldReceive('setState')
->once();
$this->historyManager
->shouldReceive('isHistoryEnabled')
->once()
->andReturn(true);
$this->historyManager
->shouldReceive('getHistory')
->once()
->andReturn([]);
// Expect history entry with only 'count' in changed properties
$this->historyManager
->shouldReceive('addHistoryEntry')
->once()
->with(
componentId: Mockery::any(),
stateData: Mockery::any(),
stateClass: Mockery::any(),
version: Mockery::any(),
changeType: Mockery::any(),
context: Mockery::any(),
changedProperties: Mockery::on(function ($changed) {
// Only count changed
return is_array($changed)
&& count($changed) === 1
&& in_array('count', $changed);
}),
previousChecksum: Mockery::any(),
currentChecksum: Mockery::any()
);
// Persist state
$this->persistence->persistState($component, $newState, 'testAction');
});
it('uses CREATED change type for new state', function () {
$componentId = new ComponentId('tracked-comp', 'instance-4');
$newState = new TestPersistenceState(count: 1, name: 'new');
$component = new TestTrackedComponent($componentId, $newState);
// Mock: No previous state exists
$this->stateManager
->shouldReceive('getState')
->once()
->andReturn(null);
$this->stateManager
->shouldReceive('setState')
->once();
$this->historyManager
->shouldReceive('isHistoryEnabled')
->once()
->andReturn(true);
$this->historyManager
->shouldReceive('getHistory')
->once()
->andReturn([]);
// Expect CREATED change type
$this->historyManager
->shouldReceive('addHistoryEntry')
->once()
->with(
componentId: Mockery::any(),
stateData: Mockery::any(),
stateClass: Mockery::any(),
version: Mockery::any(),
changeType: 'created', // New state
context: Mockery::any(),
changedProperties: Mockery::any(),
previousChecksum: null, // No previous checksum
currentChecksum: Mockery::type('string')
);
// Persist state
$this->persistence->persistState($component, $newState, 'testAction');
});
it('respects TrackStateHistory configuration', function () {
// Component with selective tracking
#[TrackStateHistory(
trackIpAddress: false, // Disabled
trackUserAgent: false, // Disabled
trackChangedProperties: true
)]
final readonly class SelectiveComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public TestPersistenceState $state
) {}
}
$componentId = new ComponentId('selective-comp', 'instance-5');
$newState = new TestPersistenceState(count: 1, name: 'test');
$component = new SelectiveComponent($componentId, $newState);
// Mock setup
$this->stateManager
->shouldReceive('getState')
->once()
->andReturn(null);
$this->stateManager
->shouldReceive('setState')
->once();
$this->historyManager
->shouldReceive('isHistoryEnabled')
->once()
->andReturn(true);
$this->historyManager
->shouldReceive('getHistory')
->once()
->andReturn([]);
// Expect context WITHOUT ip_address and user_agent
$this->historyManager
->shouldReceive('addHistoryEntry')
->once()
->with(
componentId: Mockery::any(),
stateData: Mockery::any(),
stateClass: Mockery::any(),
version: Mockery::any(),
changeType: Mockery::any(),
context: Mockery::on(function ($context) {
// Should have user_id and session_id, but NOT ip_address or user_agent
return isset($context['user_id'])
&& isset($context['session_id'])
&& !isset($context['ip_address'])
&& !isset($context['user_agent']);
}),
changedProperties: Mockery::any(),
previousChecksum: Mockery::any(),
currentChecksum: Mockery::any()
);
// Persist state
$this->persistence->persistState($component, $newState, 'testAction');
});
});
});