Repository: hotmeteor/spectator (php)

Issue:
Request body required for file uploads in Laravel tests
Hello! My case is following for uploading single file in Laravel tests. Error first:

```
ErrorException: Request body required.
Failed asserting that true is false.
/var/www/html/vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php:123
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1753
/var/www/html/vendor/hotmeteor/spectator/src/Assertions.php:26
/var/www/html/vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php:123
/var/www/html/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:1753
/var/www/html/tests/Feature/Assignment/AssignmentUploadTest.php:58
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:174
```

 I defined Swagger as follows:

```
<?php

namespace App\Swagger\Actions\Assignment;

use OpenApi\Annotations as OA;

/**
 * @OA\Post(
 *     path="/excel/upload",
 *     summary="Upload Excel (XLSX) file with assignments",
 *     tags={"assignments"},
 *     security={{"bearerAuth": {}}},
 *
 *     @OA\RequestBody(
 *         required=true,
 *         description="Uploads Excel file with assignments",
 *         @OA\MediaType(
 *             mediaType="multipart/form-data",
 *             @OA\Schema(
 *                 @OA\Property(property="file", type="string", format="binary"),
 *             )
 *         )
 *     ),
 *
 *     @OA\Response(
 *         response=204,
 *         description="Success message"
 *     ),
 *
 *     @OA\Response(response=401, description="Unauthorized"),
 *     @OA\Response(response=403, description="Forbidden"),
 * )
 */
class Upload
{
}
```

Test in Laravel looks more or less like this:

```
public function testValidUploadWithFile(): void
{
    $file = UploadedFile::fake()->create('test_file.xlsx', $this->contentFile);
    $response = $this->actingAs(User::factory()->admin()->create(), 'sanctum')
        ->post(route('excel.upload'), ['file' => $file]);

    $response
        ->assertValidRequest() // TODO: Fix Swagger's Upload.php to define multipart/form-data as we get here "ErrorException: Request body required."
        ->assertValidResponse(204); // It works
}
```

And I get this error I mentioned at the beginning. Isn't it some incorrect validation in Spectator? I think Laravel builds multipart request for this, but I get `ErrorException: Request body required.`

Pull request description:
Fix request validation for request only having uploaded file
Fixes #154 

Patch:
diff --git a/src/Validation/RequestValidator.php b/src/Validation/RequestValidator.php
index d4bba26..f874713 100644
--- a/src/Validation/RequestValidator.php
+++ b/src/Validation/RequestValidator.php
@@ -149,12 +149,6 @@ class RequestValidator extends AbstractValidator
     protected function validateBody(): void
     {
         $expectedBody = $this->operation()->requestBody;
-        $actualBody = $this->request->getContent();
-
-        // If required, then body should be non-empty.
-        if ($expectedBody->required === true && empty($actualBody)) {
-            throw new RequestValidationException('Request body required.');
-        }
 
         // Content types should match.
         $contentType = $this->request->header('Content-Type');
@@ -164,14 +158,20 @@ class RequestValidator extends AbstractValidator
 
         // Capture schemas for validation.
         $expectedBodyRawSchema = $expectedBody->content[$contentType]->schema;
-        $actualBodySchema = $actualBody;
-        if ($expectedBodyRawSchema->type === 'object' || $expectedBodyRawSchema->type === 'array' || $expectedBodyRawSchema->oneOf || $expectedBodyRawSchema->anyOf) {
-            if (in_array($contentType, ['application/json', 'application/vnd.api+json'])) {
-                $actualBodySchema = json_decode($actualBodySchema);
-            } else {
-                $actualBodySchema = $this->parseBodySchema();
-            }
+        if (
+            ($expectedBodyRawSchema->type === 'object' || $expectedBodyRawSchema->type === 'array' || $expectedBodyRawSchema->oneOf || $expectedBodyRawSchema->anyOf)
+            && in_array($contentType, ['application/json', 'application/vnd.api+json'])
+        ) {
+            $actualBodySchema = json_decode($this->request->getContent());
+        } else {
+            $actualBodySchema = $this->parseBodySchema();
         }
+
+        // If required, then body should be non-empty.
+        if ($expectedBody->required === true && empty($actualBodySchema)) {
+            throw new RequestValidationException('Request body required!');
+        }
+
         $expectedBodySchema = $this->prepareData($expectedBodyRawSchema, 'write');
 
         // Run validation.


Test patch:
diff --git a/tests/Fixtures/Upload.yml b/tests/Fixtures/Upload.yml
new file mode 100644
index 0000000..7c8c35e
--- /dev/null
+++ b/tests/Fixtures/Upload.yml
@@ -0,0 +1,27 @@
+openapi: 3.0.0
+info:
+  title: Upload.v1
+  version: '1.0'
+servers:
+  - url: 'http://localhost:3000'
+paths:
+  /upload:
+    post:
+      requestBody:
+        required: true
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                file:
+                  type: string
+                  format: binary
+              required:
+                - file
+      responses:
+        '204':
+          description: No Content
+
+components:
+  schemas: {}
diff --git a/tests/RequestValidatorTest.php b/tests/RequestValidatorTest.php
index d330073..8a3449e 100644
--- a/tests/RequestValidatorTest.php
+++ b/tests/RequestValidatorTest.php
@@ -565,7 +565,11 @@ class RequestValidatorTest extends TestCase
 
         $this->post(
             '/users',
-            ['name' => 'Adam Campbell', 'email' => 'test@test.com', 'picture' => UploadedFile::fake()->image('test.jpg')],
+            [
+                'name' => 'Adam Campbell',
+                'email' => 'test@test.com',
+                'picture' => UploadedFile::fake()->image('test.jpg'),
+            ],
             ['Content-Type' => 'multipart/form-data']
         )
             ->assertValidRequest();
@@ -838,6 +842,18 @@ class RequestValidatorTest extends TestCase
             ],
         ];
     }
+
+    public function test_validates_upload_file(): void
+    {
+        Spectator::using('Upload.yml');
+
+        Route::post('/upload', function () {
+            return response()->noContent();
+        })->middleware(Middleware::class);
+
+        $this->post('/upload', ['file' => UploadedFile::fake()->createWithContent('test.xlsx', 'Content')], ['Content-Type' => 'multipart/form-data'])
+            ->assertValidRequest();
+    }
 }
 
 class TestUser extends Model