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; } }