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:
350
tests/Framework/Http/Parser/StreamingParserTest.php
Normal file
350
tests/Framework/Http/Parser/StreamingParserTest.php
Normal file
@@ -0,0 +1,350 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user