Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,715 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Http\Parser;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Http\Parser\Exception\ParserSecurityException;
use App\Framework\Http\Parser\FileUploadParser;
use App\Framework\Http\Parser\ParserConfig;
use App\Framework\Http\UploadError;
use PHPUnit\Framework\TestCase;
final class FileUploadParserTest extends TestCase
{
private FileUploadParser $parser;
protected function setUp(): void
{
$this->parser = new FileUploadParser();
}
protected function tearDown(): void
{
// Clean up any temporary files created during tests
$tempDir = sys_get_temp_dir();
$files = glob($tempDir . '/upload_*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
public function testParseMultipartSingleFile(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Hello World!\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$this->assertCount(1, $result->all());
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('test.txt', $file->name);
$this->assertSame('text/plain', $file->type);
$this->assertSame(12, $file->size); // "Hello World!" length
$this->assertSame(UploadError::OK, $file->error);
$this->assertTrue(file_exists($file->tmpName));
$this->assertSame('Hello World!', file_get_contents($file->tmpName));
}
public function testParseMultipartMultipleFiles(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content 1\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content 2\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$this->assertCount(2, $result->all());
$file1 = $result->get('file1');
$file2 = $result->get('file2');
$this->assertNotNull($file1);
$this->assertNotNull($file2);
$this->assertSame('test1.txt', $file1->name);
$this->assertSame('test2.txt', $file2->name);
$this->assertSame('Content 1', file_get_contents($file1->tmpName));
$this->assertSame('Content 2', file_get_contents($file2->tmpName));
}
public function testParseMultipartFileArray(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"files[]\"; filename=\"file1.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"File 1\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"files[]\"; filename=\"file2.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"File 2\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$files = $result->get('files');
$this->assertIsArray($files);
$this->assertCount(2, $files);
$this->assertSame('file1.txt', $files[0]->name);
$this->assertSame('file2.txt', $files[1]->name);
$this->assertSame('File 1', file_get_contents($files[0]->tmpName));
$this->assertSame('File 2', file_get_contents($files[1]->tmpName));
}
public function testParseMultipartNestedFileArray(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"docs[legal][]\"; filename=\"contract.pdf\"\r\n" .
"Content-Type: application/pdf\r\n" .
"\r\n" .
"%PDF-1.4 PDF content here\r\n" . // Add PDF signature to match MIME type
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"docs[images][0]\"; filename=\"logo.png\"\r\n" .
"Content-Type: image/png\r\n" .
"\r\n" .
"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" . "PNG content here\r\n" . // Add PNG signature
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$docs = $result->get('docs');
$this->assertIsArray($docs);
$this->assertArrayHasKey('legal', $docs);
$this->assertArrayHasKey('images', $docs);
$legalFiles = $docs['legal'];
$this->assertIsArray($legalFiles);
$this->assertCount(1, $legalFiles);
$this->assertSame('contract.pdf', $legalFiles[0]->name);
$this->assertSame('application/pdf', $legalFiles[0]->type);
$imageFiles = $docs['images'];
$this->assertIsArray($imageFiles);
$this->assertSame('logo.png', $imageFiles['0']->name);
$this->assertSame('image/png', $imageFiles['0']->type);
}
public function testParseMultipartWithoutFilename(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"data\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Just regular form data\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
// Should not create any files for parts without filename
$this->assertCount(0, $result->all());
}
public function testParseMultipartDefaultContentType(): void
{
// Use a parser with relaxed security for this test
$config = new ParserConfig(validateFileExtensions: false, scanForMaliciousContent: false);
$parser = new FileUploadParser($config);
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"binary.dat\"\r\n" .
"\r\n" .
"Binary data here\r\n" .
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('application/octet-stream', $file->type);
}
public function testParseMultipartRfc2231ExtendedFilename(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename*=UTF-8''caf%C3%A9.txt\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('café.txt', $file->name); // Should be properly decoded
}
public function testParseMultipartRfc2231InvalidFormat(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename*=invalid_format\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('invalid_format', $file->name); // Should return as-is for invalid format
}
public function testParseMultipartEmptyFile(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"empty.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('empty.txt', $file->name);
$this->assertSame(0, $file->size);
$this->assertSame('', file_get_contents($file->tmpName));
}
public function testParseMultipartMalformedParts(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Malformed part without proper headers\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"valid\"; filename=\"test.txt\"\r\n" .
"\r\n" .
"Valid content\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
// Should ignore malformed parts and process valid ones
$this->assertCount(1, $result->all());
$file = $result->get('valid');
$this->assertNotNull($file);
$this->assertSame('test.txt', $file->name);
}
public function testParseMultipartMissingName(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; filename=\"test.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
// Should ignore parts without name attribute
$this->assertCount(0, $result->all());
}
public function testParseMultipartQuotedValues(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"my file.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Content with spaces in filename\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('my file.txt', $file->name);
}
public function testParseMultipartLargeFile(): void
{
$boundary = '----FormBoundary123';
$largeContent = str_repeat('A', 10000); // 10KB of 'A's
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"large\"; filename=\"large.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
$largeContent . "\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('large');
$this->assertNotNull($file);
$this->assertSame(10000, $file->size);
$this->assertSame($largeContent, file_get_contents($file->tmpName));
}
public function testTemporaryFileCleanup(): void
{
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" .
"Content-Type: text/plain\r\n" .
"\r\n" .
"Test content\r\n" .
"------FormBoundary123--\r\n";
$result = $this->parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$tmpPath = $file->tmpName;
$this->assertTrue(file_exists($tmpPath));
// Simulate script end - the shutdown function should clean up
// We can't easily test this automatically, but the file path is registered
// for cleanup in the shutdown function
$this->assertStringStartsWith(sys_get_temp_dir() . '/upload_', $tmpPath);
}
// Security Tests
public function testBoundaryTooLong(): void
{
$config = new ParserConfig(maxBoundaryLength: 10);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Multipart boundary too long');
$longBoundary = str_repeat('a', 15);
$body = "--{$longBoundary}\r\nContent-Disposition: form-data; name=\"test\"; filename=\"test.txt\"\r\n\r\nvalue\r\n--{$longBoundary}--\r\n";
$parser->parseMultipart($body, $longBoundary);
}
public function testFileCountExceeded(): void
{
$config = new ParserConfig(maxFileCount: 2);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('File count exceeded');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n\r\nContent 1\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"\r\n\r\nContent 2\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file3\"; filename=\"test3.txt\"\r\n\r\nContent 3\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testFileSizeExceeded(): void
{
$config = new ParserConfig(maxFileSize: new Byte(50));
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('File size exceeded');
$boundary = '----FormBoundary123';
$largeContent = str_repeat('a', 100); // Exceeds 50 byte limit
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"large.txt\"\r\n\r\n" .
"{$largeContent}\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testTotalUploadSizeExceeded(): void
{
$config = new ParserConfig(maxTotalUploadSize: new Byte(100));
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Total upload size exceeded');
$boundary = '----FormBoundary123';
$content1 = str_repeat('a', 60); // 60 bytes
$content2 = str_repeat('b', 50); // 50 bytes - total 110 bytes > 100 limit
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n" .
"{$content1}\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file2\"; filename=\"file2.txt\"\r\n\r\n" .
"{$content2}\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testBlockedFileExtension(): void
{
$config = new ParserConfig(blockedFileExtensions: ['php', 'exe']);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('File extension blocked');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"malicious.php\"\r\n\r\n" .
"<?php echo 'hack'; ?>\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testFileExtensionNotAllowed(): void
{
$config = new ParserConfig(allowedFileExtensions: ['txt', 'jpg']);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('File extension not allowed');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"document.pdf\"\r\n\r\n" .
"PDF content\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testMaliciousExecutableContent(): void
{
$config = new ParserConfig(
scanForMaliciousContent: true,
allowedFileExtensions: ['exe', 'txt'], // Allow exe to test content validation
blockedFileExtensions: [] // Remove blocked extensions to test content
);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Executable content detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"malware.exe\"\r\n\r\n" .
"\x4D\x5A" . str_repeat("x", 100) . "\r\n" . // PE executable signature
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testMaliciousPhpContent(): void
{
$config = new ParserConfig(
scanForMaliciousContent: true,
allowedFileExtensions: ['txt', 'php'], // Allow txt to test content validation
blockedFileExtensions: [] // Remove blocked extensions to test content
);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('PHP code detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"shell.txt\"\r\n\r\n" .
"<?php system(\$_GET['cmd']); ?>\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testMaliciousScriptContent(): void
{
$config = new ParserConfig(
scanForMaliciousContent: true,
allowedFileExtensions: ['html', 'txt'], // Allow html to test content validation
blockedFileExtensions: [] // Remove blocked extensions to test content
);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Suspicious script content detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"xss.html\"\r\n\r\n" .
"<script>alert('xss')</script>\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testMaliciousEvalContent(): void
{
$config = new ParserConfig(
scanForMaliciousContent: true,
allowedFileExtensions: ['js', 'txt'], // Allow js to test content validation
blockedFileExtensions: [] // Remove blocked extensions to test content
);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Suspicious script content detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"evil.js\"\r\n\r\n" .
"eval(atob('YWxlcnQoJ2hhY2snKQ=='))\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testMimeTypeMismatch(): void
{
$config = new ParserConfig(scanForMaliciousContent: true, strictMimeTypeValidation: true);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('MIME type mismatch');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"fake.jpg\"\r\n" .
"Content-Type: image/jpeg\r\n\r\n" .
"%PDF-1.4 This is actually a PDF file\r\n" . // PDF signature but claiming to be JPEG
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testShellScriptDetection(): void
{
$config = new ParserConfig(scanForMaliciousContent: true);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Executable content detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"script.txt\"\r\n\r\n" .
"#!/bin/bash\nrm -rf /\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
public function testSystemCallDetection(): void
{
$config = new ParserConfig(scanForMaliciousContent: true);
$parser = new FileUploadParser($config);
$this->expectException(ParserSecurityException::class);
$this->expectExceptionMessage('Suspicious script content detected');
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"backdoor.txt\"\r\n\r\n" .
"system('cat /etc/passwd')\r\n" .
"------FormBoundary123--\r\n";
$parser->parseMultipart($body, $boundary);
}
// Security Configuration Tests
public function testSecurityDisabled(): void
{
$config = new ParserConfig(
scanForMaliciousContent: false,
validateFileExtensions: false,
allowedFileExtensions: [], // Empty to allow all when validation disabled
blockedFileExtensions: [], // Empty to not block anything when validation disabled
maxFileCount: 1000,
maxFileSize: new Byte(10 * 1024 * 1024),
maxTotalUploadSize: new Byte(100 * 1024 * 1024)
);
$parser = new FileUploadParser($config);
// Should not throw exception when security is disabled
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"malicious.php\"\r\n\r\n" .
"<?php system(\$_GET['cmd']); ?>\r\n" .
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('malicious.php', $file->name);
}
public function testWithinSecurityLimits(): void
{
$config = new ParserConfig(
maxFileCount: 5,
maxFileSize: new Byte(1024),
maxTotalUploadSize: new Byte(5 * 1024),
allowedFileExtensions: ['txt', 'jpg', 'png'],
scanForMaliciousContent: true
);
$parser = new FileUploadParser($config);
// Should work fine within limits
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n\r\n" .
"Safe content 1\r\n" .
"------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"file2\"; filename=\"image.jpg\"\r\n\r\n" .
"\xFF\xD8\xFF" . str_repeat('x', 10) . "\r\n" . // Valid JPEG signature
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$this->assertCount(2, $result->all());
$file1 = $result->get('file1');
$file2 = $result->get('file2');
$this->assertNotNull($file1);
$this->assertNotNull($file2);
$this->assertSame('test1.txt', $file1->name);
$this->assertSame('image.jpg', $file2->name);
}
public function testAllowedExtensionsEmptyList(): void
{
$config = new ParserConfig(
allowedFileExtensions: [], // Empty list should allow all (except blocked)
blockedFileExtensions: ['exe'],
scanForMaliciousContent: false
);
$parser = new FileUploadParser($config);
// Should allow PDF when allowed list is empty
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"document.pdf\"\r\n\r\n" .
"PDF content\r\n" .
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('document.pdf', $file->name);
}
public function testCompatibleMimeTypes(): void
{
$config = new ParserConfig(scanForMaliciousContent: true, strictMimeTypeValidation: true);
$parser = new FileUploadParser($config);
// Should allow compatible MIME types (image/jpg vs image/jpeg)
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"photo.jpg\"\r\n" .
"Content-Type: image/jpg\r\n\r\n" .
"\xFF\xD8\xFF" . str_repeat('x', 10) . "\r\n" . // Valid JPEG signature
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('photo.jpg', $file->name);
}
public function testNoExtensionFile(): void
{
$config = new ParserConfig(validateFileExtensions: true, allowedFileExtensions: ['txt']);
$parser = new FileUploadParser($config);
// Should allow files without extension (no validation performed)
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"README\"\r\n\r\n" .
"This is a README file\r\n" .
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('README', $file->name);
}
public function testEmptyFilename(): void
{
$config = new ParserConfig(validateFileExtensions: true, allowedFileExtensions: ['txt']);
$parser = new FileUploadParser($config);
// Should allow empty filename (no validation performed)
$boundary = '----FormBoundary123';
$body = "------FormBoundary123\r\n" .
"Content-Disposition: form-data; name=\"upload\"; filename=\"\"\r\n\r\n" .
"Content without filename\r\n" .
"------FormBoundary123--\r\n";
$result = $parser->parseMultipart($body, $boundary);
$file = $result->get('upload');
$this->assertNotNull($file);
$this->assertSame('', $file->name);
}
}