- 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.
388 lines
13 KiB
PHP
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);
|
|
});
|
|
});
|