Files
michaelschiemer/tests/Framework/Http/Parser/StreamingParserTest.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

351 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Http\Parser;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Http\Parser\ParserConfig;
use App\Framework\Http\Parser\StreamingParser;
use PHPUnit\Framework\TestCase;
/**
* Tests for the streaming multipart parser with generators
*/
final class StreamingParserTest extends TestCase
{
private StreamingParser $parser;
protected function setUp(): void
{
$this->parser = new StreamingParser();
}
protected function tearDown(): void
{
// Clean up any temp files
$tempFiles = glob(sys_get_temp_dir() . '/upload_*');
foreach ($tempFiles as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
public function testStreamSimpleFormField(): void
{
$boundary = 'boundary123';
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"field1\"\r\n";
$data .= "\r\n";
$data .= "value1\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
fclose($stream);
$this->assertCount(1, $parts);
$this->assertEquals('field', $parts[0]['type']);
$this->assertEquals('field1', $parts[0]['name']);
$this->assertEquals('value1', $parts[0]['data']);
}
public function testStreamMultipleFields(): void
{
$boundary = 'boundary123';
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"field1\"\r\n";
$data .= "\r\n";
$data .= "value1\r\n";
$data .= "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"field2\"\r\n";
$data .= "\r\n";
$data .= "value2\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
fclose($stream);
$this->assertCount(2, $parts);
$this->assertEquals('field1', $parts[0]['name']);
$this->assertEquals('value1', $parts[0]['data']);
$this->assertEquals('field2', $parts[1]['name']);
$this->assertEquals('value2', $parts[1]['data']);
}
public function testStreamFileUpload(): void
{
$boundary = 'boundary123';
$fileContent = "This is the file content\nWith multiple lines";
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n";
$data .= "Content-Type: text/plain\r\n";
$data .= "\r\n";
$data .= $fileContent . "\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
fclose($stream);
$this->assertCount(1, $parts);
$this->assertEquals('file', $parts[0]['type']);
$this->assertEquals('upload', $parts[0]['name']);
$this->assertEquals('test.txt', $parts[0]['filename']);
$this->assertIsResource($parts[0]['stream']);
// Read content from temp file stream
$content = stream_get_contents($parts[0]['stream']);
fclose($parts[0]['stream']);
$this->assertEquals($fileContent . "\r\n", $content);
}
public function testStreamLargeFile(): void
{
$boundary = 'boundary123';
// Generate 1MB of data
$fileContent = str_repeat('A', 1024 * 1024);
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"bigfile\"; filename=\"large.bin\"\r\n";
$data .= "Content-Type: application/octet-stream\r\n";
$data .= "\r\n";
$data .= $fileContent . "\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
// Should stream without loading entire file into memory
$memoryBefore = memory_get_usage();
$parts = [];
foreach ($this->parser->streamMultipart($stream, $boundary) as $part) {
$parts[] = $part;
// Memory usage should not increase significantly
$memoryDuring = memory_get_usage();
$memoryIncrease = $memoryDuring - $memoryBefore;
// Should use less than 100KB extra memory for streaming
$this->assertLessThan(
100 * 1024,
$memoryIncrease,
'Streaming should not load entire file into memory'
);
}
fclose($stream);
$this->assertCount(1, $parts);
$this->assertEquals('file', $parts[0]['type']);
$this->assertEquals('bigfile', $parts[0]['name']);
// Verify file size
$stats = fstat($parts[0]['stream']);
$this->assertEquals(strlen($fileContent) + 2, $stats['size']); // +2 for CRLF
fclose($parts[0]['stream']);
}
public function testStreamMixedContent(): void
{
$boundary = 'boundary123';
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"text\"\r\n";
$data .= "\r\n";
$data .= "Some text value\r\n";
$data .= "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"file\"; filename=\"doc.pdf\"\r\n";
$data .= "Content-Type: application/pdf\r\n";
$data .= "\r\n";
$data .= "%PDF-1.4 fake pdf content\r\n";
$data .= "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"another\"\r\n";
$data .= "\r\n";
$data .= "Another field\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
fclose($stream);
$this->assertCount(3, $parts);
// First part - text field
$this->assertEquals('field', $parts[0]['type']);
$this->assertEquals('text', $parts[0]['name']);
$this->assertEquals('Some text value', $parts[0]['data']);
// Second part - file
$this->assertEquals('file', $parts[1]['type']);
$this->assertEquals('file', $parts[1]['name']);
$this->assertEquals('doc.pdf', $parts[1]['filename']);
// Third part - another field
$this->assertEquals('field', $parts[2]['type']);
$this->assertEquals('another', $parts[2]['name']);
$this->assertEquals('Another field', $parts[2]['data']);
// Cleanup
if (isset($parts[1]['stream'])) {
fclose($parts[1]['stream']);
}
}
public function testParseFilesFromStream(): void
{
$boundary = 'boundary123';
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"files[0]\"; filename=\"file1.txt\"\r\n";
$data .= "Content-Type: text/plain\r\n";
$data .= "\r\n";
$data .= "File 1 content\r\n";
$data .= "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"files[1]\"; filename=\"file2.txt\"\r\n";
$data .= "Content-Type: text/plain\r\n";
$data .= "\r\n";
$data .= "File 2 content\r\n";
$data .= "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"avatar\"; filename=\"user.png\"\r\n";
$data .= "Content-Type: image/png\r\n";
$data .= "\r\n";
$data .= "PNG fake content\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$files = $this->parser->parseFilesFromStream($stream, $boundary);
fclose($stream);
$this->assertArrayHasKey('files', $files);
$this->assertArrayHasKey('avatar', $files);
$this->assertIsArray($files['files']);
$this->assertCount(2, $files['files']);
$this->assertEquals('file1.txt', $files['files'][0]->name);
$this->assertEquals('file2.txt', $files['files'][1]->name);
$this->assertEquals('user.png', $files['avatar']->name);
// Verify content
$this->assertEquals("File 1 content\r\n", file_get_contents($files['files'][0]->tmpName));
$this->assertEquals("File 2 content\r\n", file_get_contents($files['files'][1]->tmpName));
}
public function testMaxFileCountLimit(): void
{
$config = new ParserConfig(maxFileCount: 2);
$parser = new StreamingParser($config);
$boundary = 'boundary123';
$data = "--boundary123\r\n";
// Add 3 files to exceed limit
for ($i = 1; $i <= 3; $i++) {
$data .= "Content-Disposition: form-data; name=\"file$i\"; filename=\"file$i.txt\"\r\n";
$data .= "\r\n";
$data .= "Content $i\r\n";
$data .= "--boundary123\r\n";
}
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
$this->expectExceptionMessage('Maximum number of parts exceeded');
// Consume all parts to trigger exception
$parts = iterator_to_array($parser->streamMultipart($stream, $boundary));
fclose($stream);
}
public function testFileSizeLimit(): void
{
$config = new ParserConfig(maxFileSize: Byte::fromKilobytes(1)); // 1KB limit
$parser = new StreamingParser($config);
$boundary = 'boundary123';
// Create file larger than 1KB
$largeContent = str_repeat('X', 2048); // 2KB
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"file\"; filename=\"large.txt\"\r\n";
$data .= "\r\n";
$data .= $largeContent . "\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
$this->expectExceptionMessage('File size exceeded');
// Consume parts to trigger exception
foreach ($parser->streamMultipart($stream, $boundary) as $part) {
// Exception should be thrown when finalizing the part
}
fclose($stream);
}
public function testFieldValueLengthLimit(): void
{
$config = new ParserConfig(maxFieldValueLength: 10);
$parser = new StreamingParser($config);
$boundary = 'boundary123';
$data = "--boundary123\r\n";
$data .= "Content-Disposition: form-data; name=\"field\"\r\n";
$data .= "\r\n";
$data .= "This value is too long\r\n";
$data .= "--boundary123--\r\n";
$stream = $this->createStream($data);
$this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
$this->expectExceptionMessage('Field value too long');
foreach ($parser->streamMultipart($stream, $boundary) as $part) {
// Exception should be thrown while reading field data
}
fclose($stream);
}
public function testInvalidStreamResource(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('First parameter must be a valid stream resource');
// Pass non-resource
foreach ($this->parser->streamMultipart('not a stream', 'boundary') as $part) {
// Should throw before yielding
}
}
public function testEmptyStream(): void
{
$stream = $this->createStream('');
$parts = iterator_to_array($this->parser->streamMultipart($stream, 'boundary'));
fclose($stream);
$this->assertCount(0, $parts);
}
/**
* Create an in-memory stream from string data
*/
private function createStream(string $data)
{
$stream = fopen('php://memory', 'r+');
fwrite($stream, $data);
rewind($stream);
return $stream;
}
}