- 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.
294 lines
11 KiB
PHP
294 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\FileSize;
|
|
use App\Framework\Filesystem\FileValidator;
|
|
use App\Framework\Filesystem\Exceptions\FileValidationException;
|
|
|
|
describe('FileValidator', function () {
|
|
// Factory Methods Tests
|
|
it('creates default validator with safe defaults', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
expect($validator->getAllowedExtensions())->toBeNull();
|
|
expect($validator->getBlockedExtensions())->toBe(['exe', 'bat', 'sh', 'cmd', 'com']);
|
|
expect($validator->getMaxFileSize())->toBeInstanceOf(FileSize::class);
|
|
expect($validator->getMaxFileSize()->toBytes())->toBe(100 * 1024 * 1024); // 100MB
|
|
});
|
|
|
|
it('creates strict validator with allowed extensions only', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
expect($validator->getAllowedExtensions())->toBe(['txt', 'pdf']);
|
|
expect($validator->getBlockedExtensions())->toBeNull();
|
|
expect($validator->getMaxFileSize()->toBytes())->toBe(50 * 1024 * 1024); // 50MB
|
|
});
|
|
|
|
it('creates upload validator with secure defaults', function () {
|
|
$validator = FileValidator::forUploads();
|
|
|
|
expect($validator->getAllowedExtensions())->toBe(['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'csv', 'json']);
|
|
expect($validator->getBlockedExtensions())->toBe(['exe', 'bat', 'sh', 'cmd', 'com', 'php', 'phtml']);
|
|
expect($validator->getMaxFileSize()->toBytes())->toBe(10 * 1024 * 1024); // 10MB
|
|
});
|
|
|
|
it('creates image validator with image extensions only', function () {
|
|
$validator = FileValidator::forImages();
|
|
|
|
expect($validator->getAllowedExtensions())->toBe(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']);
|
|
expect($validator->getBlockedExtensions())->toBeNull();
|
|
expect($validator->getMaxFileSize()->toBytes())->toBe(5 * 1024 * 1024); // 5MB
|
|
});
|
|
|
|
it('allows custom max size for uploads', function () {
|
|
$customSize = FileSize::fromMegabytes(20);
|
|
$validator = FileValidator::forUploads($customSize);
|
|
|
|
expect($validator->getMaxFileSize()->toBytes())->toBe(20 * 1024 * 1024);
|
|
});
|
|
|
|
// Path Validation Tests
|
|
it('validates normal file paths', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
$validator->validatePath('/var/www/html/test.txt');
|
|
$validator->validatePath('relative/path/file.json');
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
it('throws exception for empty path', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validatePath('');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('File path cannot be empty');
|
|
}
|
|
});
|
|
|
|
it('throws exception for path with null bytes', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validatePath("/path/to/file\0.txt");
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('null bytes');
|
|
}
|
|
});
|
|
|
|
it('detects path traversal with ../', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validatePath('../../../etc/passwd');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('Path traversal attempt');
|
|
}
|
|
});
|
|
|
|
it('detects path traversal with backslash notation', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validatePath('..\\..\\windows\\system32');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('Path traversal attempt');
|
|
}
|
|
});
|
|
|
|
it('detects URL-encoded path traversal', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validatePath('/path/%2e%2e/etc/passwd');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('Path traversal attempt');
|
|
}
|
|
});
|
|
|
|
// Extension Validation Tests
|
|
it('validates allowed extensions', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
$validator->validateExtension('/path/to/file.txt');
|
|
$validator->validateExtension('/path/to/document.pdf');
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
it('throws exception for disallowed extension', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
try {
|
|
$validator->validateExtension('/path/to/file.exe');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('not allowed');
|
|
}
|
|
});
|
|
|
|
it('throws exception for blocked extension', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
try {
|
|
$validator->validateExtension('/path/to/malware.exe');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('is blocked');
|
|
}
|
|
});
|
|
|
|
it('allows files without extension when no whitelist defined', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
$validator->validateExtension('/path/to/Makefile');
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
it('throws exception for files without extension when whitelist defined', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
try {
|
|
$validator->validateExtension('/path/to/Makefile');
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('no extension');
|
|
}
|
|
});
|
|
|
|
it('handles case-insensitive extension validation', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
$validator->validateExtension('/path/to/file.TXT');
|
|
$validator->validateExtension('/path/to/document.PDF');
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
// FileSize Validation Tests
|
|
it('validates file size within limit', function () {
|
|
$validator = FileValidator::forUploads(FileSize::fromMegabytes(10));
|
|
|
|
$validator->validateFileSize(FileSize::fromMegabytes(5));
|
|
$validator->validateFileSize(FileSize::fromMegabytes(9));
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
it('throws exception when file size exceeds limit', function () {
|
|
$validator = FileValidator::forUploads(FileSize::fromMegabytes(10));
|
|
|
|
try {
|
|
$validator->validateFileSize(FileSize::fromMegabytes(15));
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('exceeds maximum allowed size');
|
|
}
|
|
});
|
|
|
|
it('allows any file size when no limit defined', function () {
|
|
$validator = new FileValidator(
|
|
allowedExtensions: null,
|
|
blockedExtensions: null,
|
|
maxFileSize: null,
|
|
baseDirectory: null
|
|
);
|
|
|
|
$validator->validateFileSize(FileSize::fromGigabytes(10));
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
// Extension Check Helper Tests
|
|
it('checks if extension is allowed', function () {
|
|
$validator = FileValidator::createStrict(['txt', 'pdf']);
|
|
|
|
expect($validator->isExtensionAllowed('txt'))->toBeTrue();
|
|
expect($validator->isExtensionAllowed('.pdf'))->toBeTrue(); // With dot
|
|
expect($validator->isExtensionAllowed('TXT'))->toBeTrue(); // Case insensitive
|
|
expect($validator->isExtensionAllowed('exe'))->toBeFalse();
|
|
});
|
|
|
|
it('checks blocked extensions correctly', function () {
|
|
$validator = FileValidator::createDefault();
|
|
|
|
expect($validator->isExtensionAllowed('exe'))->toBeFalse();
|
|
expect($validator->isExtensionAllowed('txt'))->toBeTrue();
|
|
expect($validator->isExtensionAllowed('pdf'))->toBeTrue();
|
|
});
|
|
|
|
it('allows all extensions when no restrictions', function () {
|
|
$validator = new FileValidator();
|
|
|
|
expect($validator->isExtensionAllowed('exe'))->toBeTrue();
|
|
expect($validator->isExtensionAllowed('anything'))->toBeTrue();
|
|
});
|
|
|
|
// Composite Validation Tests
|
|
it('validates upload with all checks', function () {
|
|
$validator = FileValidator::forUploads();
|
|
|
|
$validator->validateUpload('/path/to/image.jpg', FileSize::fromMegabytes(2));
|
|
|
|
expect(true)->toBeTrue(); // No exception thrown
|
|
});
|
|
|
|
it('composite upload validation catches path traversal', function () {
|
|
$validator = FileValidator::forUploads();
|
|
|
|
try {
|
|
$validator->validateUpload('../../../etc/passwd.jpg', FileSize::fromMegabytes(1));
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('Path traversal');
|
|
}
|
|
});
|
|
|
|
it('composite upload validation catches disallowed extension', function () {
|
|
$validator = FileValidator::forUploads();
|
|
|
|
try {
|
|
$validator->validateUpload('/path/to/script.php', FileSize::fromMegabytes(1));
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('is not allowed');
|
|
}
|
|
});
|
|
|
|
it('composite upload validation catches oversized file', function () {
|
|
$validator = FileValidator::forUploads();
|
|
|
|
try {
|
|
$validator->validateUpload('/path/to/image.jpg', FileSize::fromMegabytes(50));
|
|
expect(true)->toBeFalse('Should have thrown exception');
|
|
} catch (FileValidationException $e) {
|
|
expect($e->getMessage())->toContain('exceeds maximum');
|
|
}
|
|
});
|
|
|
|
// Getter Tests
|
|
it('provides access to configuration', function () {
|
|
$allowedExtensions = ['txt', 'pdf'];
|
|
$blockedExtensions = ['exe', 'bat'];
|
|
$maxSize = FileSize::fromMegabytes(20);
|
|
|
|
$validator = new FileValidator(
|
|
allowedExtensions: $allowedExtensions,
|
|
blockedExtensions: $blockedExtensions,
|
|
maxFileSize: $maxSize
|
|
);
|
|
|
|
expect($validator->getAllowedExtensions())->toBe($allowedExtensions);
|
|
expect($validator->getBlockedExtensions())->toBe($blockedExtensions);
|
|
expect($validator->getMaxFileSize())->toBe($maxSize);
|
|
});
|
|
});
|