Files
michaelschiemer/tests/Integration/Framework/LiveComponents/DatabaseStateIntegrationTest.php.skip

401 lines
14 KiB
Plaintext

<?php
declare(strict_types=1);
use App\Framework\Cache\SmartCache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\EntityManager;
use App\Framework\Database\Schema\Schema;
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\LiveComponents\ValueObjects\ComponentRenderData;
use App\Framework\StateManagement\Database\DatabaseStateHistoryManager;
use App\Framework\StateManagement\Database\DatabaseStateManager;
use App\Framework\StateManagement\SerializableState;
// Test State
final readonly class IntegrationTestState implements SerializableState
{
public function __construct(
public int $counter = 0,
public string $message = '',
public array $items = []
) {}
public function toArray(): array
{
return [
'counter' => $this->counter,
'message' => $this->message,
'items' => $this->items,
];
}
public static function fromArray(array $data): self
{
return new self(
counter: $data['counter'] ?? 0,
message: $data['message'] ?? '',
items: $data['items'] ?? []
);
}
}
// Test Component with History
#[TrackStateHistory(
trackIpAddress: true,
trackUserAgent: true,
trackChangedProperties: true,
maxHistoryEntries: 10
)]
final readonly class IntegrationTestComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public IntegrationTestState $state
) {}
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'components/integration-test',
data: ['state' => $this->state]
);
}
}
describe('Database State Integration', function () {
beforeEach(function () {
// Real dependencies for integration test
// Use in-memory SQLite for testing
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->connection = new class($pdo) implements ConnectionInterface {
public function __construct(private PDO $pdo) {}
public function getPdo(): PDO
{
return $this->pdo;
}
public function query(string $sql, array $params = []): array
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function execute(string $sql, array $params = []): int
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
public function lastInsertId(): string
{
return $this->pdo->lastInsertId();
}
public function beginTransaction(): void
{
$this->pdo->beginTransaction();
}
public function commit(): void
{
$this->pdo->commit();
}
public function rollback(): void
{
$this->pdo->rollBack();
}
public function inTransaction(): bool
{
return $this->pdo->inTransaction();
}
};
$this->entityManager = new EntityManager($this->connection);
$this->cache = new SmartCache();
// Create state manager
$this->stateManager = new DatabaseStateManager(
entityManager: $this->entityManager,
cache: $this->cache,
stateClass: IntegrationTestState::class,
logger: null,
cacheTtl: Duration::fromSeconds(60)
);
// Create history manager
$this->historyManager = new DatabaseStateHistoryManager(
entityManager: $this->entityManager,
logger: null
);
// Create request context
$this->requestContext = new RequestContext(
userId: 'test-user-123',
sessionId: 'test-session-456',
ipAddress: '127.0.0.1',
userAgent: 'Test-Agent/1.0'
);
// Create persistence handler
$this->persistence = new LiveComponentStatePersistence(
stateManager: $this->stateManager,
historyManager: $this->historyManager,
requestContext: $this->requestContext,
logger: null
);
// Setup database tables
// Create component_state table
$this->connection->execute("
CREATE TABLE component_state (
component_id TEXT PRIMARY KEY,
state_data TEXT NOT NULL,
state_class TEXT NOT NULL,
component_name TEXT NOT NULL,
user_id TEXT,
session_id TEXT,
version INTEGER DEFAULT 1,
checksum TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
expires_at TEXT
)
");
// Create component_state_history table
$this->connection->execute("
CREATE TABLE component_state_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
component_id TEXT NOT NULL,
state_data TEXT NOT NULL,
state_class TEXT NOT NULL,
version INTEGER NOT NULL,
change_type TEXT NOT NULL,
changed_properties TEXT,
user_id TEXT,
session_id TEXT,
ip_address TEXT,
user_agent TEXT,
previous_checksum TEXT,
current_checksum TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (component_id) REFERENCES component_state(component_id) ON DELETE CASCADE
)
");
});
afterEach(function () {
// Cleanup test tables
$this->connection->execute("DROP TABLE IF EXISTS component_state_history");
$this->connection->execute("DROP TABLE IF EXISTS component_state");
});
it('persists component state to database', function () {
$componentId = new ComponentId('counter', 'test-1');
$state = new IntegrationTestState(
counter: 42,
message: 'Hello Integration Test',
items: ['item1', 'item2']
);
$component = new IntegrationTestComponent($componentId, $state);
// Persist state
$this->persistence->persistState($component, $state, 'increment');
// Verify state was saved
$retrieved = $this->stateManager->getState($componentId->toString());
expect($retrieved)->toBeInstanceOf(IntegrationTestState::class);
expect($retrieved->counter)->toBe(42);
expect($retrieved->message)->toBe('Hello Integration Test');
expect($retrieved->items)->toBe(['item1', 'item2']);
});
it('tracks state changes in history', function () {
$componentId = new ComponentId('counter', 'test-2');
$component = new IntegrationTestComponent(
$componentId,
new IntegrationTestState(counter: 0, message: 'initial')
);
// Create initial state
$state1 = new IntegrationTestState(counter: 0, message: 'initial');
$this->persistence->persistState($component, $state1, 'init');
// Update state
$state2 = new IntegrationTestState(counter: 1, message: 'updated');
$component = new IntegrationTestComponent($componentId, $state2);
$this->persistence->persistState($component, $state2, 'increment');
// Update again
$state3 = new IntegrationTestState(counter: 2, message: 'updated again');
$component = new IntegrationTestComponent($componentId, $state3);
$this->persistence->persistState($component, $state3, 'increment');
// Get history
$history = $this->historyManager->getHistory($componentId->toString());
expect($history)->toBeArray();
expect(count($history))->toBeGreaterThanOrEqual(2);
// Verify history entries are ordered DESC
expect($history[0]->version)->toBeGreaterThan($history[1]->version);
// Verify context was captured
expect($history[0]->userId)->toBe('test-user-123');
expect($history[0]->sessionId)->toBe('test-session-456');
expect($history[0]->ipAddress)->toBe('127.0.0.1');
expect($history[0]->userAgent)->toBe('Test-Agent/1.0');
});
it('uses cache for fast retrieval after initial load', function () {
$componentId = new ComponentId('counter', 'test-3');
$state = new IntegrationTestState(counter: 99, message: 'cached');
$component = new IntegrationTestComponent($componentId, $state);
// First persist (cold)
$this->persistence->persistState($component, $state, 'test');
// Get state (should populate cache)
$retrieved1 = $this->stateManager->getState($componentId->toString());
// Get state again (should hit cache)
$retrieved2 = $this->stateManager->getState($componentId->toString());
expect($retrieved1->counter)->toBe(99);
expect($retrieved2->counter)->toBe(99);
// Verify cache statistics show hits
$stats = $this->stateManager->getStatistics();
expect($stats->hitCount)->toBeGreaterThan(0);
});
it('tracks changed properties correctly', function () {
$componentId = new ComponentId('counter', 'test-4');
// Initial state
$state1 = new IntegrationTestState(
counter: 10,
message: 'first',
items: ['a', 'b']
);
$component = new IntegrationTestComponent($componentId, $state1);
$this->persistence->persistState($component, $state1, 'init');
// Update only counter
$state2 = new IntegrationTestState(
counter: 11,
message: 'first', // Same
items: ['a', 'b'] // Same
);
$component = new IntegrationTestComponent($componentId, $state2);
$this->persistence->persistState($component, $state2, 'increment');
// Get latest history entry
$history = $this->historyManager->getHistory($componentId->toString(), limit: 1);
$latestEntry = $history[0];
// Should only track 'counter' as changed
expect($latestEntry->changedProperties)->toBeArray();
expect($latestEntry->changedProperties)->toContain('counter');
expect(count($latestEntry->changedProperties))->toBe(1);
});
it('supports atomic state updates', function () {
$componentId = new ComponentId('counter', 'test-5');
$initialState = new IntegrationTestState(counter: 0, message: 'start');
$component = new IntegrationTestComponent($componentId, $initialState);
// Persist initial state
$this->persistence->persistState($component, $initialState, 'init');
// Atomic update
$updatedState = $this->stateManager->updateState(
$componentId->toString(),
fn(IntegrationTestState $state) => new IntegrationTestState(
counter: $state->counter + 5,
message: 'updated',
items: $state->items
)
);
expect($updatedState)->toBeInstanceOf(IntegrationTestState::class);
expect($updatedState->counter)->toBe(5);
expect($updatedState->message)->toBe('updated');
});
it('retrieves specific version from history', function () {
$componentId = new ComponentId('counter', 'test-6');
// Create multiple versions
for ($i = 1; $i <= 5; $i++) {
$state = new IntegrationTestState(
counter: $i,
message: "version {$i}"
);
$component = new IntegrationTestComponent($componentId, $state);
$this->persistence->persistState($component, $state, 'update');
}
// Get version 3
$version3 = $this->historyManager->getHistoryByVersion($componentId->toString(), 3);
expect($version3)->not->toBeNull();
expect($version3->version)->toBe(3);
$state3 = IntegrationTestState::fromArray(json_decode($version3->stateData, true));
expect($state3->counter)->toBe(3);
expect($state3->message)->toBe('version 3');
});
it('cleans up old history entries', function () {
$componentId = new ComponentId('counter', 'test-7');
// Create 10 history entries
for ($i = 1; $i <= 10; $i++) {
$state = new IntegrationTestState(counter: $i);
$component = new IntegrationTestComponent($componentId, $state);
$this->persistence->persistState($component, $state, 'update');
}
// Keep only last 5
$deleted = $this->historyManager->cleanup($componentId->toString(), keepLast: 5);
expect($deleted)->toBe(5);
// Verify only 5 entries remain
$history = $this->historyManager->getHistory($componentId->toString());
expect(count($history))->toBe(5);
});
it('provides statistics about state storage', function () {
// Create multiple components
for ($i = 1; $i <= 3; $i++) {
$componentId = new ComponentId('counter', "test-stats-{$i}");
$state = new IntegrationTestState(counter: $i);
$component = new IntegrationTestComponent($componentId, $state);
$this->persistence->persistState($component, $state, 'create');
}
// Get statistics
$stats = $this->stateManager->getStatistics();
expect($stats->totalKeys)->toBeGreaterThanOrEqual(3);
expect($stats->setCount)->toBeGreaterThanOrEqual(3);
});
});