Disparse multipart/form-data in the Laravel test set
-
I want to write a PHPUnit-test for a bunch of forms that were inherited. The solution must therefore be serial.
What we did.
- I'm filling up and sending the form on the website.
- I'm doing "copy as curl" in the browser's console.
- I'm using the data I' the PHPUnit-test in Laravel.
This is the minimum working example:
public function testMinimal(): void { $headers = [ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8", "Accept" => "application/json, text/javascript, */*; q=0.01" ];
$data = "form%5Bkey%5D=val"; // данные берутся из "скопировать как curl" parse_str($data, $request); // это ["form[key]" => "val"] $resp = $this->post("test", $request, $headers); $resp->assertOk()->assertJson([ "form" => ["key" => "val"] ]);
}
api.php:
Route::post("test", function () {
// эхо-сервер возвращает то, что пришло
return response(app("request")->toArray())->header("Content-Type", "application/json");
});
I did it for forms.
application/x-www-form-urlencoded
♪ Here you go.multipart/form-data
There is a problem - there is no suitable method in Laravel. I want something like that.$boundary = "---------------------------134321616315313667391972112742";
$body = "-----------------------------134321616315313667391972112742\r\nContent-Disposition: form-data; name ..."; // тут я обрезал
$resp = $this->postMultipart("test", $body, $boundary, $headers);
How do I do that?
-
I did what I wanted. We need to:
- Disparse.
multipart/form-data
with a packageriverline/multipart-parser
part - Walk in parts and transform into a mass maintained by Laravel
It's important. Turns out that "copy like cURL" doesn't keep the contents of the downloaded files, and you need to copy the "Question." The request itself looks like:
-----------------------------15879248054246973702996273273 Content-Disposition: form-data; name="feedback[text]"
text
-----------------------------15879248054246973702996273273
Content-Disposition: form-data; name="feedback[file_1]"; filename="file.pdf"
Content-Type: application/pdfcontent of the file...
-----------------------------15879248054246973702996273273
Content-Disposition: form-data; name="is_draft"true
-----------------------------15879248054246973702996273273--
Big files can't be copied this way, browser says the request was down. In this case, it is possible to keep a raw request on a web server or to be tested on file ~100 Cb.
I wrote down below how to arrange such testing.
Set the package:
composer require --dev riverline/multipart-parser
Transformation class
multipart/form-data
in the test area - fileMultipartFormDataParser.php
:namespace Tests\TestCase\MultipartFormDataParser;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use Riverline\MultiPartParser\Converters\PSR7;
use Riverline\MultiPartParser\StreamedPart;class MultipartFormDataParser
{
private array $headers;
private string $body;
private Request $request;
private StreamedPart $parts;
private array $data;public function setHeaders(array $headers): self { $this->headers = $headers; return $this; } public function getHeaders(): array { return $this->headers; } public function setBody(string $body): self { $this->body = $body; return $this; } public function getData(): array { return $this->data; } public function guessBoundary(): self { $firstString = strtok($this->body, "\n"); $boundary = Str::replaceFirst("--", "", $firstString); $this->headers = array_filter($this->headers, fn($o) => Str::lower($o) !== "content-type", ARRAY_FILTER_USE_KEY); $this->headers["Content-Type"] = "multipart/form-data; boundary=$boundary"; return $this; } public function parse(): self { $this->request = new Request("POST", "http://localhost", $this->headers, $this->body); $this->parts = PSR7::convert($this->request); $this->data = []; foreach ($this->parts->getParts() as $part) { if ($part->isFile()) { $item = UploadedFile::fake()->createWithContent($part->getFileName(), $part->getBody()); $item->mimeType($part->getMimeType()); } else { $item = $part->getBody(); } $data = $this->parseKeyAndSetVal($part->getName(), $item); $this->data = array_merge_recursive($this->data, $data); } return $this; } private function parseKeyAndSetVal($key, $val): array { parse_str($key, $res); array_walk_recursive($res, function (&$child) use ($val) { if ($child === "") { $child = $val; } }, $val); return $res; }
}
Test
MinTest.php
where the password is used:class MinTest extends TestCase
{
public function testMin(): void
{
$headers = [
// Заголовок Content-Type будет заменен в guessBoundary(), он тут не обязателен
"Content-Type" => "multipart/form-data; boundary=guess-boundary",
"Accept" => "application/json, text/javascript, /; q=0.01"
];$body = "..."; // тут body запроса, взято из dev tools браузера, сеть, вкладка "Запрос" $parser = new MultipartFormDataParser; $parser->setHeaders($headers)->setBody($body)->guessBoundary()->parse(); $data = $parser->getData(); $resp = $this->post("test", $data, $parser->getHeaders()); dd($resp->json()); }
}
Echo server to check:
Route::post("test", function () {
// эхо-сервер возвращает то, что пришло
/** @var \Illuminate\Http\Request $request */
$request = app("request");
return response([
"data" => $request->post(),
"files" => array_keys($request->allFiles())
])->header("Content-Type", "application/json");
});
- Disparse.