Files
michaelschiemer/tests/Unit/Framework/LiveComponents/InMemoryUploadSessionStoreTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

388 lines
13 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\LiveComponents\Services\UploadSessionIdGenerator;
use App\Framework\LiveComponents\ValueObjects\ChunkMetadata;
use App\Framework\LiveComponents\ValueObjects\ChunkHash;
use App\Framework\LiveComponents\ValueObjects\UploadSession;
use App\Framework\LiveComponents\ValueObjects\UploadSessionId;
use App\Framework\LiveComponents\ValueObjects\QuarantineStatus;
use App\Framework\Random\RandomGenerator;
use Tests\Support\InMemoryUploadSessionStore;
describe('InMemoryUploadSessionStore', function () {
beforeEach(function () {
$this->store = new InMemoryUploadSessionStore();
// Create RandomGenerator for UploadSessionId generation
$this->randomGen = new class implements RandomGenerator {
public function bytes(int $length): string
{
return random_bytes($length);
}
public function int(int $min, int $max): int
{
return random_int($min, $max);
}
public function float(): float
{
return (float) random_int(0, PHP_INT_MAX) / PHP_INT_MAX;
}
};
$this->sessionIdGenerator = new UploadSessionIdGenerator($this->randomGen);
});
it('saves and retrieves session', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
$this->store->save($session);
$retrieved = $this->store->get($sessionId);
expect($retrieved !== null)->toBeTrue();
expect($retrieved->sessionId->equals($sessionId))->toBeTrue();
expect($retrieved->fileName)->toBe('test.pdf');
expect($retrieved->totalChunks)->toBe(10);
});
it('returns null for non-existent session', function () {
$sessionId = $this->sessionIdGenerator->generate();
$retrieved = $this->store->get($sessionId);
expect($retrieved)->toBeNull();
});
it('checks session existence correctly', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
expect($this->store->exists($sessionId))->toBeFalse();
$this->store->save($session);
expect($this->store->exists($sessionId))->toBeTrue();
});
it('deletes session', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
$this->store->save($session);
expect($this->store->exists($sessionId))->toBeTrue();
$this->store->delete($sessionId);
expect($this->store->exists($sessionId))->toBeFalse();
expect($this->store->get($sessionId))->toBeNull();
});
it('deletes non-existent session without error', function () {
$sessionId = $this->sessionIdGenerator->generate();
expect($this->store->exists($sessionId))->toBeFalse();
$this->store->delete($sessionId);
expect($this->store->exists($sessionId))->toBeFalse();
});
it('updates existing session', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
$this->store->save($session);
// Add chunk to session
$chunk = ChunkMetadata::create(
index: 0,
size: Byte::fromKilobytes(512),
hash: ChunkHash::fromData('chunk data')
);
$updatedSession = $session->withChunk($chunk);
$this->store->save($updatedSession);
$retrieved = $this->store->get($sessionId);
expect($retrieved !== null)->toBeTrue();
expect(count($retrieved->chunks))->toBe(1);
expect($retrieved->chunks[0]->index)->toBe(0);
});
it('cleans up expired sessions', function () {
// Create expired session (TTL in past)
$expiredSessionId = $this->sessionIdGenerator->generate();
$expiredSession = new UploadSession(
sessionId: $expiredSessionId,
componentId: 'test-component',
fileName: 'expired.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10,
createdAt: new DateTimeImmutable('-2 hours'),
expiresAt: new DateTimeImmutable('-1 hour')
);
// Create valid session
$validSessionId = $this->sessionIdGenerator->generate();
$validSession = UploadSession::create(
sessionId: $validSessionId,
componentId: 'test-component',
fileName: 'valid.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
$this->store->save($expiredSession);
$this->store->save($validSession);
expect($this->store->count())->toBe(2);
$cleaned = $this->store->cleanupExpired();
expect($cleaned)->toBe(1);
expect($this->store->count())->toBe(1);
expect($this->store->exists($expiredSessionId))->toBeFalse();
expect($this->store->exists($validSessionId))->toBeTrue();
});
it('cleanup returns zero when no expired sessions', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
$this->store->save($session);
$cleaned = $this->store->cleanupExpired();
expect($cleaned)->toBe(0);
expect($this->store->count())->toBe(1);
});
it('cleanup works on empty store', function () {
expect($this->store->count())->toBe(0);
$cleaned = $this->store->cleanupExpired();
expect($cleaned)->toBe(0);
expect($this->store->count())->toBe(0);
});
it('handles multiple sessions', function () {
$sessions = [];
for ($i = 0; $i < 5; $i++) {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: "component-{$i}",
fileName: "file-{$i}.pdf",
totalSize: Byte::fromMegabytes($i + 1),
totalChunks: ($i + 1) * 2
);
$sessions[] = $session;
$this->store->save($session);
}
expect($this->store->count())->toBe(5);
foreach ($sessions as $session) {
$retrieved = $this->store->get($session->sessionId);
expect($retrieved !== null)->toBeTrue();
expect($retrieved->sessionId->equals($session->sessionId))->toBeTrue();
}
});
it('getAll returns all sessions as array', function () {
$session1 = UploadSession::create(
sessionId: $this->sessionIdGenerator->generate(),
componentId: 'component-1',
fileName: 'file1.pdf',
totalSize: Byte::fromMegabytes(1),
totalChunks: 2
);
$session2 = UploadSession::create(
sessionId: $this->sessionIdGenerator->generate(),
componentId: 'component-2',
fileName: 'file2.pdf',
totalSize: Byte::fromMegabytes(2),
totalChunks: 4
);
$this->store->save($session1);
$this->store->save($session2);
$allSessions = $this->store->getAll();
expect($allSessions)->toBeArray();
expect(count($allSessions))->toBe(2);
expect($allSessions[0])->toBeInstanceOf(UploadSession::class);
expect($allSessions[1])->toBeInstanceOf(UploadSession::class);
});
it('count returns correct session count', function () {
expect($this->store->count())->toBe(0);
$session1 = UploadSession::create(
sessionId: $this->sessionIdGenerator->generate(),
componentId: 'component-1',
fileName: 'file1.pdf',
totalSize: Byte::fromMegabytes(1),
totalChunks: 2
);
$this->store->save($session1);
expect($this->store->count())->toBe(1);
$session2 = UploadSession::create(
sessionId: $this->sessionIdGenerator->generate(),
componentId: 'component-2',
fileName: 'file2.pdf',
totalSize: Byte::fromMegabytes(2),
totalChunks: 4
);
$this->store->save($session2);
expect($this->store->count())->toBe(2);
$this->store->delete($session1->sessionId);
expect($this->store->count())->toBe(1);
});
it('clear removes all sessions', function () {
for ($i = 0; $i < 3; $i++) {
$session = UploadSession::create(
sessionId: $this->sessionIdGenerator->generate(),
componentId: "component-{$i}",
fileName: "file-{$i}.pdf",
totalSize: Byte::fromMegabytes($i + 1),
totalChunks: ($i + 1) * 2
);
$this->store->save($session);
}
expect($this->store->count())->toBe(3);
$this->store->clear();
expect($this->store->count())->toBe(0);
expect($this->store->getAll())->toBe([]);
});
it('handles session with quarantine status', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 10
);
// Proper lifecycle: PENDING → SCANNING → APPROVED
$scanningSession = $session->withQuarantineStatus(QuarantineStatus::SCANNING);
$approvedSession = $scanningSession->withQuarantineStatus(QuarantineStatus::APPROVED);
$this->store->save($approvedSession);
$retrieved = $this->store->get($sessionId);
expect($retrieved !== null)->toBeTrue();
expect($retrieved->quarantineStatus)->toBe(QuarantineStatus::APPROVED);
});
it('handles session with chunks', function () {
$sessionId = $this->sessionIdGenerator->generate();
$session = UploadSession::create(
sessionId: $sessionId,
componentId: 'test-component',
fileName: 'test.pdf',
totalSize: Byte::fromMegabytes(5),
totalChunks: 3
);
// Add chunks
$chunk1 = ChunkMetadata::create(
index: 0,
size: Byte::fromKilobytes(512),
hash: ChunkHash::fromData('chunk 0 data')
);
$chunk2 = ChunkMetadata::create(
index: 1,
size: Byte::fromKilobytes(512),
hash: ChunkHash::fromData('chunk 1 data')
);
$sessionWithChunks = $session->withChunk($chunk1)->withChunk($chunk2);
$this->store->save($sessionWithChunks);
$retrieved = $this->store->get($sessionId);
expect($retrieved !== null)->toBe(true);
expect(count($retrieved->chunks))->toBe(2);
});
it('isolates sessions by different session IDs', function () {
$sessionId1 = $this->sessionIdGenerator->generate();
$sessionId2 = $this->sessionIdGenerator->generate();
$session1 = UploadSession::create(
sessionId: $sessionId1,
componentId: 'component-1',
fileName: 'file1.pdf',
totalSize: Byte::fromMegabytes(1),
totalChunks: 2
);
$session2 = UploadSession::create(
sessionId: $sessionId2,
componentId: 'component-2',
fileName: 'file2.pdf',
totalSize: Byte::fromMegabytes(2),
totalChunks: 4
);
$this->store->save($session1);
$this->store->save($session2);
$retrieved1 = $this->store->get($sessionId1);
$retrieved2 = $this->store->get($sessionId2);
expect($retrieved1->fileName)->toBe('file1.pdf');
expect($retrieved2->fileName)->toBe('file2.pdf');
expect($retrieved1->totalChunks)->toBe(2);
expect($retrieved2->totalChunks)->toBe(4);
});
});