diff --git a/src/Symfony/Component/BrowserKit/HttpBrowser.php b/src/Symfony/Component/BrowserKit/HttpBrowser.php index b2331ea492a84..30cd7a79071ce 100644 --- a/src/Symfony/Component/BrowserKit/HttpBrowser.php +++ b/src/Symfony/Component/BrowserKit/HttpBrowser.php @@ -73,18 +73,10 @@ private function getBodyAndExtraHeaders(Request $request): array } $fields = $request->getParameters(); - $hasFile = false; - foreach ($request->getFiles() as $name => $file) { - if (!isset($file['tmp_name'])) { - continue; - } - - $hasFile = true; - $fields[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); - } + $uploadedFiles = $this->getUploadedFiles($request->getFiles()); - if ($hasFile) { - $part = new FormDataPart($fields); + if (count($uploadedFiles) > 0) { + $part = new FormDataPart($uploadedFiles); return [$part->bodyToIterable(), $part->getPreparedHeaders()->toArray()]; } @@ -119,4 +111,28 @@ private function getHeaders(Request $request): array return $headers; } + + /** + * Recursively go through the list. If the file has a tmp_name, convert it to a DataPart. + * Keep the original hierarchy. + * + * @param array $files + * @return array + */ + private function getUploadedFiles(array $files) + { + $uploadedFiles = []; + foreach ($files as $name => $file) { + if (!is_array($file)) { + return $uploadedFiles; + } + if (!isset($file['tmp_name'])) { + $uploadedFiles[$name] = $this->getUploadedFiles($file); + } + if (isset($file['tmp_name'])) { + $uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); + } + } + return $uploadedFiles; + } } diff --git a/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php b/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php index 44eed997bdeed..326d1a67c62db 100644 --- a/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php @@ -26,18 +26,20 @@ public function getBrowser(array $server = [], History $history = null, CookieJa /** * @dataProvider validContentTypes + * @param array $requestArguments + * @param array $expectedArguments */ - public function testRequestHeaders(array $request, array $exepectedCall) + public function testRequestHeaders(array $requestArguments, array $expectedArguments) { $client = $this->createMock(HttpClientInterface::class); $client ->expects($this->once()) ->method('request') - ->with(...$exepectedCall) + ->with(...$expectedArguments) ->willReturn($this->createMock(ResponseInterface::class)); $browser = new HttpBrowser($client); - $browser->request(...$request); + $browser->request(...$requestArguments); } public function validContentTypes() @@ -61,7 +63,7 @@ public function validContentTypes() ]; } - public function testMultiPartRequest() + public function testMultiPartRequestWithSingleFile(): void { $client = $this->createMock(HttpClientInterface::class); $client @@ -81,4 +83,95 @@ public function testMultiPartRequest() file_put_contents($path, 'my_file'); $browser->request('POST', 'http://example.com/', [], ['file' => ['tmp_name' => $path, 'name' => 'foo']]); } + + public function testMultiPartRequestWithNormalFlatArray(): void + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => $this->getUploadedFile('file2'), + ]); + } + + public function testMultiPartRequestWithNormalNestedArray(): void + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'level1' => [ + 'level2' => [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => $this->getUploadedFile('file2'), + ] + ], + ]); + } + + public function testMultiPartRequestWithBracketedArray(): void + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'form[file1]' => $this->getUploadedFile('file1'), + 'form[file2]' => $this->getUploadedFile('file2'), + ]); + } + + public function testMultiPartRequestWithInvalidItem(): void + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => 'INVALID', + ]); + } + + private function uploadFile(string $data): string + { + $path = tempnam(sys_get_temp_dir(), 'http'); + file_put_contents($path, $data); + return $path; + } + + /** + * @return array + */ + private function getUploadedFile(string $name): array + { + return [ + 'tmp_name' => $this->uploadFile($name.'_content'), + 'name' => $name.'_name', + ]; + } + + /** + * @param string[] $fileContents + */ + protected function expectClientToSendRequestWithFiles(HttpClientInterface $client, $fileContents): void + { + $client + ->expects($this->once()) + ->method('request') + ->with('POST', 'http://example.com/', $this->callback(function ($options) use ($fileContents) { + $this->assertStringContainsString('Content-Type: multipart/form-data', implode('', $options['headers'])); + $this->assertInstanceOf('\Generator', $options['body']); + $body = implode('', iterator_to_array($options['body'], false)); + foreach ($fileContents as $content) { + $this->assertStringContainsString($content, $body); + } + + return true; + })) + ->willReturn($this->createMock(ResponseInterface::class)); + } }