fileStorage = new InMemoryStorage(); $this->service = new QuarantineService( fileStorage: $this->fileStorage, quarantinePath: '/tmp/quarantine', defaultRetentionPeriod: Duration::fromHours(24) ); }); test('quarantines file successfully', function () { // Create source file $sourcePath = '/tmp/source/test-file.txt'; $this->fileStorage->put($sourcePath, 'test content'); // Quarantine file $quarantineId = 'test-quarantine-123'; $quarantinePath = $this->service->quarantine($sourcePath, $quarantineId); // Verify file moved to quarantine expect($this->fileStorage->exists($quarantinePath))->toBeTrue(); expect($this->fileStorage->exists($sourcePath))->toBeFalse(); expect($quarantinePath)->toBe('/tmp/quarantine/' . $quarantineId); }); test('throws exception when quarantining non-existent file', function () { $nonExistentPath = '/tmp/does-not-exist.txt'; $quarantineId = 'test-quarantine-123'; expect(fn() => $this->service->quarantine($nonExistentPath, $quarantineId)) ->toThrow(\InvalidArgumentException::class, 'Source file not found'); }); test('releases quarantined file to target location', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/file.txt'; $this->fileStorage->put($sourcePath, 'quarantined content'); $quarantineId = 'release-test-456'; $this->service->quarantine($sourcePath, $quarantineId); // Release file $targetPath = '/tmp/target/released-file.txt'; $this->service->release($quarantineId, $targetPath); // Verify file moved to target expect($this->fileStorage->exists($targetPath))->toBeTrue(); expect($this->service->exists($quarantineId))->toBeFalse(); expect($this->fileStorage->get($targetPath))->toBe('quarantined content'); }); test('throws exception when releasing non-existent quarantined file', function () { $nonExistentQuarantineId = 'does-not-exist'; $targetPath = '/tmp/target/file.txt'; expect(fn() => $this->service->release($nonExistentQuarantineId, $targetPath)) ->toThrow(\InvalidArgumentException::class, 'Quarantined file not found'); }); test('deletes quarantined file', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/to-delete.txt'; $this->fileStorage->put($sourcePath, 'delete me'); $quarantineId = 'delete-test-789'; $this->service->quarantine($sourcePath, $quarantineId); // Delete quarantined file $this->service->delete($quarantineId); // Verify file deleted expect($this->service->exists($quarantineId))->toBeFalse(); }); test('delete handles non-existent quarantine gracefully', function () { $nonExistentId = 'never-existed'; // Should not throw exception $this->service->delete($nonExistentId); expect($this->service->exists($nonExistentId))->toBeFalse(); }); test('checks quarantine existence correctly', function () { $sourcePath = '/tmp/source/exist-check.txt'; $this->fileStorage->put($sourcePath, 'existence test'); $quarantineId = 'exist-test-101'; // Before quarantine expect($this->service->exists($quarantineId))->toBeFalse(); // After quarantine $this->service->quarantine($sourcePath, $quarantineId); expect($this->service->exists($quarantineId))->toBeTrue(); // After deletion $this->service->delete($quarantineId); expect($this->service->exists($quarantineId))->toBeFalse(); }); test('gets correct quarantine path', function () { $quarantineId = 'path-test-202'; $expectedPath = '/tmp/quarantine/' . $quarantineId; $actualPath = $this->service->getQuarantinePath($quarantineId); expect($actualPath)->toBe($expectedPath); }); test('scans file with clean result', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/clean-file.txt'; $this->fileStorage->put($sourcePath, 'clean content'); $quarantineId = 'scan-clean-303'; $this->service->quarantine($sourcePath, $quarantineId); // Mock scanner that returns clean $scannerHook = function (string $filePath): ScanResult { return ScanResult::clean('No threats detected'); }; // Scan file $result = $this->service->scan($quarantineId, $scannerHook); expect($result->isClean())->toBeTrue(); expect($result->status)->toBe(ScanStatus::CLEAN); }); test('scans file with infected result', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/infected-file.txt'; $this->fileStorage->put($sourcePath, 'infected content'); $quarantineId = 'scan-infected-404'; $this->service->quarantine($sourcePath, $quarantineId); // Mock scanner that detects threat $scannerHook = function (string $filePath): ScanResult { return ScanResult::infected( threatName: 'Trojan.Generic', confidenceScore: 0.95, details: 'Malicious payload detected' ); }; // Scan file $result = $this->service->scan($quarantineId, $scannerHook); expect($result->isInfected())->toBeTrue(); expect($result->threatName)->toBe('Trojan.Generic'); expect($result->confidenceScore)->toBe(0.95); }); test('scans file with suspicious result', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/suspicious-file.txt'; $this->fileStorage->put($sourcePath, 'suspicious content'); $quarantineId = 'scan-suspicious-505'; $this->service->quarantine($sourcePath, $quarantineId); // Mock scanner that finds suspicious patterns $scannerHook = function (string $filePath): ScanResult { return ScanResult::suspicious( details: 'Potentially unwanted program', confidenceScore: 0.65 ); }; // Scan file $result = $this->service->scan($quarantineId, $scannerHook); expect($result->isSuspicious())->toBeTrue(); expect($result->shouldQuarantine())->toBeTrue(); }); test('throws exception when scanning non-existent quarantined file', function () { $nonExistentId = 'never-scanned'; $scannerHook = fn($path) => ScanResult::clean(); expect(fn() => $this->service->scan($nonExistentId, $scannerHook)) ->toThrow(\InvalidArgumentException::class, 'Quarantined file not found'); }); test('throws exception when scanner hook returns invalid type', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/invalid-scanner.txt'; $this->fileStorage->put($sourcePath, 'content'); $quarantineId = 'invalid-scanner-606'; $this->service->quarantine($sourcePath, $quarantineId); // Invalid scanner hook that doesn't return ScanResult $invalidHook = function (string $filePath): array { return ['status' => 'clean']; }; expect(fn() => $this->service->scan($quarantineId, $invalidHook)) ->toThrow(\InvalidArgumentException::class, 'Scanner hook must return ScanResult instance'); }); test('validates status transitions correctly', function () { // Valid transitions expect($this->service->canTransition( QuarantineStatus::PENDING, QuarantineStatus::SCANNING ))->toBeTrue(); expect($this->service->canTransition( QuarantineStatus::SCANNING, QuarantineStatus::APPROVED ))->toBeTrue(); expect($this->service->canTransition( QuarantineStatus::SCANNING, QuarantineStatus::REJECTED ))->toBeTrue(); // Invalid transitions expect($this->service->canTransition( QuarantineStatus::APPROVED, QuarantineStatus::PENDING ))->toBeFalse(); expect($this->service->canTransition( QuarantineStatus::REJECTED, QuarantineStatus::APPROVED ))->toBeFalse(); }); test('cleans up expired quarantined files', function () { // Create multiple files with different ages $oldFile1 = '/tmp/source/old-file-1.txt'; $oldFile2 = '/tmp/source/old-file-2.txt'; $recentFile = '/tmp/source/recent-file.txt'; $this->fileStorage->put($oldFile1, 'old content 1'); $this->fileStorage->put($oldFile2, 'old content 2'); $this->fileStorage->put($recentFile, 'recent content'); $this->service->quarantine($oldFile1, 'old-1'); $this->service->quarantine($oldFile2, 'old-2'); $this->service->quarantine($recentFile, 'recent-1'); // Simulate old files by using expiry time in future // (files older than 48 hours should be deleted) $expiryTime = (new DateTimeImmutable())->modify('+48 hours'); $deletedCount = $this->service->cleanupExpired($expiryTime); // All files should be deleted as they're "older" than expiry time expect($deletedCount)->toBe(3); expect($this->service->exists('old-1'))->toBeFalse(); expect($this->service->exists('old-2'))->toBeFalse(); expect($this->service->exists('recent-1'))->toBeFalse(); }); test('cleanup handles empty quarantine directory gracefully', function () { // No quarantined files $deletedCount = $this->service->cleanupExpired(); expect($deletedCount)->toBe(0); }); test('creates quarantine directory if not exists', function () { $sourcePath = '/tmp/source/first-file.txt'; $this->fileStorage->put($sourcePath, 'first quarantine'); // Quarantine directory doesn't exist yet expect($this->fileStorage->exists('/tmp/quarantine'))->toBeFalse(); // Quarantine file - should create directory $this->service->quarantine($sourcePath, 'first-quarantine'); // Directory should now exist expect($this->fileStorage->exists('/tmp/quarantine'))->toBeTrue(); }); test('creates target directory if not exists during release', function () { // Setup: quarantine a file $sourcePath = '/tmp/source/release-target-test.txt'; $this->fileStorage->put($sourcePath, 'release content'); $quarantineId = 'release-dir-test'; $this->service->quarantine($sourcePath, $quarantineId); // Target directory doesn't exist $targetPath = '/tmp/new-target-dir/released-file.txt'; expect($this->fileStorage->exists('/tmp/new-target-dir'))->toBeFalse(); // Release - should create target directory $this->service->release($quarantineId, $targetPath); // Directory and file should exist expect($this->fileStorage->exists('/tmp/new-target-dir'))->toBeTrue(); expect($this->fileStorage->exists($targetPath))->toBeTrue(); });