- 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.
305 lines
11 KiB
PHP
305 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\LiveComponents;
|
|
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Filesystem\InMemoryStorage;
|
|
use App\Framework\LiveComponents\Services\QuarantineService;
|
|
use App\Framework\LiveComponents\ValueObjects\QuarantineStatus;
|
|
use App\Framework\LiveComponents\ValueObjects\ScanResult;
|
|
use App\Framework\LiveComponents\ValueObjects\ScanStatus;
|
|
use DateTimeImmutable;
|
|
|
|
beforeEach(function () {
|
|
$this->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();
|
|
});
|