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.

    1. I'm filling up and sending the form on the website.
    2. I'm doing "copy as curl" in the browser's console.
    3. 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:

    1. Disparse. multipart/form-data with a package riverline/multipart-parser part
    2. 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/pdf

    content 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 - file MultipartFormDataParser.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.phpwhere 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");
    });



Suggested Topics

  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2