From 4f2d3b7c2bb0a2724087bd0fd5827f6fafd57679 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 27 Jun 2020 14:13:33 +0100 Subject: [PATCH] Media Endpoint improvements For the code, media related stuff is now in its own controller Added support for querying the most recent file uploaded --- app/Http/Controllers/MicropubController.php | 218 ++---------------- .../Controllers/MicropubMediaController.php | 209 +++++++++++++++++ app/Http/Responses/MicropubResponses.php | 52 +++++ routes/web.php | 7 +- tests/Feature/MicropubControllerTest.php | 142 ------------ tests/Feature/MicropubMediaTest.php | 206 +++++++++++++++++ 6 files changed, 496 insertions(+), 338 deletions(-) create mode 100644 app/Http/Controllers/MicropubMediaController.php create mode 100644 app/Http/Responses/MicropubResponses.php create mode 100644 tests/Feature/MicropubMediaTest.php diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php index 43209c67..fcccaa18 100644 --- a/app/Http/Controllers/MicropubController.php +++ b/app/Http/Controllers/MicropubController.php @@ -5,20 +5,14 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Exceptions\InvalidTokenException; -use App\Jobs\ProcessMedia; -use App\Models\{Media, Place}; +use App\Http\Responses\MicropubResponses; +use App\Models\Place; use App\Services\Micropub\{HCardService, HEntryService, UpdateService}; use App\Services\TokenService; -use Exception; -use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Http\{File, JsonResponse, Response, UploadedFile}; -use Illuminate\Support\Facades\Storage; -use Intervention\Image\Exception\NotReadableException; -use Intervention\Image\ImageManager; +use Illuminate\Http\JsonResponse; use Monolog\Handler\StreamHandler; use Monolog\Logger; use MStaack\LaravelPostgis\Geometries\Point; -use Ramsey\Uuid\Uuid; class MicropubController extends Controller { @@ -44,24 +38,31 @@ class MicropubController extends Controller * then passes over the info to the relevant Service class. * * @return JsonResponse + * @throws InvalidTokenException */ public function post(): JsonResponse { try { $tokenData = $this->tokenService->validateToken(request()->input('access_token')); } catch (InvalidTokenException $e) { - return $this->invalidTokenResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->invalidTokenResponse(); } if ($tokenData->hasClaim('scope') === false) { - return $this->tokenHasNoScopeResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->tokenHasNoScopeResponse(); } $this->logMicropubRequest(request()->all()); if ((request()->input('h') == 'entry') || (request()->input('type.0') == 'h-entry')) { if (stristr($tokenData->getClaim('scope'), 'create') === false) { - return $this->insufficientScopeResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->insufficientScopeResponse(); } $location = $this->hentryService->process(request()->all(), $this->getCLientId()); @@ -73,7 +74,9 @@ class MicropubController extends Controller if (request()->input('h') == 'card' || request()->input('type.0') == 'h-card') { if (stristr($tokenData->getClaim('scope'), 'create') === false) { - return $this->insufficientScopeResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->insufficientScopeResponse(); } $location = $this->hcardService->process(request()->all()); @@ -85,7 +88,9 @@ class MicropubController extends Controller if (request()->input('action') == 'update') { if (stristr($tokenData->getClaim('scope'), 'update') === false) { - return $this->insufficientScopeResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->insufficientScopeResponse(); } return $this->updateService->process(request()->all()); @@ -112,7 +117,9 @@ class MicropubController extends Controller try { $tokenData = $this->tokenService->validateToken(request()->input('access_token')); } catch (InvalidTokenException $e) { - return $this->invalidTokenResponse(); + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->invalidTokenResponse(); } if (request()->input('q') === 'syndicate-to') { @@ -154,131 +161,11 @@ class MicropubController extends Controller ]); } - /** - * Process a media item posted to the media endpoint. - * - * @return JsonResponse - * @throws BindingResolutionException - * @throws Exception - */ - public function media(): JsonResponse - { - try { - $tokenData = $this->tokenService->validateToken(request()->input('access_token')); - } catch (InvalidTokenException $e) { - return $this->invalidTokenResponse(); - } - - if ($tokenData->hasClaim('scope') === false) { - return $this->tokenHasNoScopeResponse(); - } - - if (stristr($tokenData->getClaim('scope'), 'create') === false) { - return $this->insufficientScopeResponse(); - } - - if ((request()->hasFile('file') && request()->file('file')->isValid()) === false) { - return response()->json([ - 'response' => 'error', - 'error' => 'invalid_request', - 'error_description' => 'The uploaded file failed validation', - ], 400); - } - - $this->logMicropubRequest(request()->all()); - - $filename = $this->saveFile(request()->file('file')); - - $manager = resolve(ImageManager::class); - try { - $image = $manager->make(request()->file('file')); - $width = $image->width(); - } catch (NotReadableException $exception) { - // not an image - $width = null; - } - - $media = Media::create([ - 'token' => request()->bearerToken(), - 'path' => 'media/' . $filename, - 'type' => $this->getFileTypeFromMimeType(request()->file('file')->getMimeType()), - 'image_widths' => $width, - ]); - - // put the file on S3 initially, the ProcessMedia job may edit this - Storage::disk('s3')->putFileAs( - 'media', - new File(storage_path('app') . '/' . $filename), - $filename - ); - - ProcessMedia::dispatch($filename); - - return response()->json([ - 'response' => 'created', - 'location' => $media->url, - ], 201)->header('Location', $media->url); - } - - /** - * Return the relevant CORS headers to a pre-flight OPTIONS request. - * - * @return Response - */ - public function mediaOptionsResponse(): Response - { - return response('OK', 200); - } - - /** - * Get the file type from the mime-type of the uploaded file. - * - * @param string $mimeType - * @return string - */ - private function getFileTypeFromMimeType(string $mimeType): string - { - //try known images - $imageMimeTypes = [ - 'image/gif', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/tiff', - 'image/webp', - ]; - if (in_array($mimeType, $imageMimeTypes)) { - return 'image'; - } - //try known video - $videoMimeTypes = [ - 'video/mp4', - 'video/mpeg', - 'video/ogg', - 'video/quicktime', - 'video/webm', - ]; - if (in_array($mimeType, $videoMimeTypes)) { - return 'video'; - } - //try known audio types - $audioMimeTypes = [ - 'audio/midi', - 'audio/mpeg', - 'audio/ogg', - 'audio/x-m4a', - ]; - if (in_array($mimeType, $audioMimeTypes)) { - return 'audio'; - } - - return 'download'; - } - /** * Determine the client id from the access token sent with the request. * * @return string + * @throws InvalidTokenException */ private function getClientId(): string { @@ -295,64 +182,7 @@ class MicropubController extends Controller private function logMicropubRequest(array $request) { $logger = new Logger('micropub'); - $logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG); + $logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log'))); $logger->debug('MicropubLog', $request); } - - /** - * Save an uploaded file to the local disk. - * - * @param UploadedFile $file - * @return string - * @throws Exception - */ - private function saveFile(UploadedFile $file): string - { - $filename = Uuid::uuid4()->toString() . '.' . $file->extension(); - Storage::disk('local')->putFileAs('', $file, $filename); - - return $filename; - } - - /** - * Generate a response to be returned when the token has insufficient scope. - * - * @return JsonResponse - */ - private function insufficientScopeResponse(): JsonResponse - { - return response()->json([ - 'response' => 'error', - 'error' => 'insufficient_scope', - 'error_description' => 'The token’s scope does not have the necessary requirements.', - ], 401); - } - - /** - * Generate a response to be returned when the token is invalid. - * - * @return JsonResponse - */ - private function invalidTokenResponse(): JsonResponse - { - return response()->json([ - 'response' => 'error', - 'error' => 'invalid_token', - 'error_description' => 'The provided token did not pass validation', - ], 400); - } - - /** - * Generate a response to be returned when the token has no scope. - * - * @return JsonResponse - */ - private function tokenHasNoScopeResponse(): JsonResponse - { - return response()->json([ - 'response' => 'error', - 'error' => 'invalid_request', - 'error_description' => 'The provided token has no scopes', - ], 400); - } } diff --git a/app/Http/Controllers/MicropubMediaController.php b/app/Http/Controllers/MicropubMediaController.php new file mode 100644 index 00000000..93788e33 --- /dev/null +++ b/app/Http/Controllers/MicropubMediaController.php @@ -0,0 +1,209 @@ +tokenService = $tokenService; + } + + public function getHandler(): JsonResponse + { + try { + $tokenData = $this->tokenService->validateToken(request()->input('access_token')); + } catch (InvalidTokenException $e) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->invalidTokenResponse(); + } + + if ($tokenData->hasClaim('scope') === false) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->tokenHasNoScopeResponse(); + } + + if (Str::contains($tokenData->getClaim('scope'), 'create') === false) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->insufficientScopeResponse(); + } + + if (request()->input('q') === 'last') { + $media = Media::latest()->firstOrFail(); + + return response()->json(['url' => $media->url]); + } + } + + /** + * Process a media item posted to the media endpoint. + * + * @return JsonResponse + * @throws BindingResolutionException + * @throws Exception + */ + public function media(): JsonResponse + { + try { + $tokenData = $this->tokenService->validateToken(request()->input('access_token')); + } catch (InvalidTokenException $e) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->invalidTokenResponse(); + } + + if ($tokenData->hasClaim('scope') === false) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->tokenHasNoScopeResponse(); + } + + if (Str::contains($tokenData->getClaim('scope'), 'create') === false) { + $micropubResponses = new MicropubResponses(); + + return $micropubResponses->insufficientScopeResponse(); + } + + if (request()->hasFile('file') === false) { + return response()->json([ + 'response' => 'error', + 'error' => 'invalid_request', + 'error_description' => 'No file was sent with the request', + ], 400); + } + + if (request()->file('file')->isValid() === false) { + return response()->json([ + 'response' => 'error', + 'error' => 'invalid_request', + 'error_description' => 'The uploaded file failed validation', + ], 400); + } + + $filename = $this->saveFile(request()->file('file')); + + $manager = resolve(ImageManager::class); + try { + $image = $manager->make(request()->file('file')); + $width = $image->width(); + } catch (NotReadableException $exception) { + // not an image + $width = null; + } + + $media = Media::create([ + 'token' => request()->bearerToken(), + 'path' => 'media/' . $filename, + 'type' => $this->getFileTypeFromMimeType(request()->file('file')->getMimeType()), + 'image_widths' => $width, + ]); + + // put the file on S3 initially, the ProcessMedia job may edit this + Storage::disk('s3')->putFileAs( + 'media', + new File(storage_path('app') . '/' . $filename), + $filename + ); + + ProcessMedia::dispatch($filename); + + return response()->json([ + 'response' => 'created', + 'location' => $media->url, + ], 201)->header('Location', $media->url); + } + + /** + * Return the relevant CORS headers to a pre-flight OPTIONS request. + * + * @return Response + */ + public function mediaOptionsResponse(): Response + { + return response('OK', 200); + } + + /** + * Get the file type from the mime-type of the uploaded file. + * + * @param string $mimeType + * @return string + */ + private function getFileTypeFromMimeType(string $mimeType): string + { + //try known images + $imageMimeTypes = [ + 'image/gif', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/tiff', + 'image/webp', + ]; + if (in_array($mimeType, $imageMimeTypes)) { + return 'image'; + } + //try known video + $videoMimeTypes = [ + 'video/mp4', + 'video/mpeg', + 'video/ogg', + 'video/quicktime', + 'video/webm', + ]; + if (in_array($mimeType, $videoMimeTypes)) { + return 'video'; + } + //try known audio types + $audioMimeTypes = [ + 'audio/midi', + 'audio/mpeg', + 'audio/ogg', + 'audio/x-m4a', + ]; + if (in_array($mimeType, $audioMimeTypes)) { + return 'audio'; + } + + return 'download'; + } + + /** + * Save an uploaded file to the local disk. + * + * @param UploadedFile $file + * @return string + * @throws Exception + */ + private function saveFile(UploadedFile $file): string + { + $filename = Uuid::uuid4()->toString() . '.' . $file->extension(); + Storage::disk('local')->putFileAs('', $file, $filename); + + return $filename; + } +} diff --git a/app/Http/Responses/MicropubResponses.php b/app/Http/Responses/MicropubResponses.php new file mode 100644 index 00000000..829e5c57 --- /dev/null +++ b/app/Http/Responses/MicropubResponses.php @@ -0,0 +1,52 @@ +json([ + 'response' => 'error', + 'error' => 'insufficient_scope', + 'error_description' => 'The token’s scope does not have the necessary requirements.', + ], 401); + } + + /** + * Generate a response to be returned when the token is invalid. + * + * @return JsonResponse + */ + public function invalidTokenResponse(): JsonResponse + { + return response()->json([ + 'response' => 'error', + 'error' => 'invalid_token', + 'error_description' => 'The provided token did not pass validation', + ], 400); + } + + /** + * Generate a response to be returned when the token has no scope. + * + * @return JsonResponse + */ + public function tokenHasNoScopeResponse(): JsonResponse + { + return response()->json([ + 'response' => 'error', + 'error' => 'invalid_request', + 'error_description' => 'The provided token has no scopes', + ], 400); + } +} diff --git a/routes/web.php b/routes/web.php index de53f1a0..0551b4c6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -144,8 +144,11 @@ Route::group(['domain' => config('url.longurl')], function () { // Micropub Endpoints Route::get('api/post', 'MicropubController@get')->middleware('micropub.token'); Route::post('api/post', 'MicropubController@post')->middleware('micropub.token'); - Route::post('api/media', 'MicropubController@media')->middleware('micropub.token', 'cors')->name('media-endpoint'); - Route::options('/api/media', 'MicropubController@mediaOptionsResponse')->middleware('cors'); + Route::get('api/media', 'MicropubMediaController@getHandler')->middleware('micropub.token'); + Route::post('api/media', 'MicropubMediaController@media') + ->middleware('micropub.token', 'cors') + ->name('media-endpoint'); + Route::options('/api/media', 'MicropubMediaController@mediaOptionsResponse')->middleware('cors'); // Webmention Route::get('webmention', 'WebMentionsController@get'); diff --git a/tests/Feature/MicropubControllerTest.php b/tests/Feature/MicropubControllerTest.php index 788c5add..af4f6624 100644 --- a/tests/Feature/MicropubControllerTest.php +++ b/tests/Feature/MicropubControllerTest.php @@ -655,148 +655,6 @@ class MicropubControllerTest extends TestCase ]); } - public function test_media_endpoint_request_with_invalid_token_return_400_response() - { - $response = $this->post( - '/api/media', - [], - ['HTTP_Authorization' => 'Bearer abc123'] - ); - $response->assertStatus(400); - $response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']); - } - - public function test_media_endpoint_request_with_token_with_no_scope_returns_400_response() - { - $response = $this->post( - '/api/media', - [], - ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()] - ); - $response->assertStatus(400); - $response->assertJsonFragment(['error_description' => 'The provided token has no scopes']); - } - - public function test_media_endpoint_request_with_insufficient_token_scopes_returns_401_response() - { - $response = $this->post( - '/api/media', - [], - ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] - ); - $response->assertStatus(401); - $response->assertJsonFragment(['error_description' => 'The token’s scope does not have the necessary requirements.']); - } - - public function test_media_endpoint_upload_a_file() - { - Queue::fake(); - Storage::fake('s3'); - $file = __DIR__ . '/../aaron.png'; - - $response = $this->post( - '/api/media', - [ - 'file' => new UploadedFile($file, 'aaron.png', 'image/png', null, true), - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - - $path = parse_url($response->getData()->location, PHP_URL_PATH); - $filename = substr($path, 7); - Queue::assertPushed(ProcessMedia::class); - Storage::disk('local')->assertExists($filename); - // now remove file - unlink(storage_path('app/') . $filename); - } - - public function test_media_endpoint_upload_an_audio_file() - { - Queue::fake(); - Storage::fake('s3'); - $file = __DIR__ . '/../audio.mp3'; - - $response = $this->post( - '/api/media', - [ - 'file' => new UploadedFile($file, 'audio.mp3', 'audio/mpeg', null, true), - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - - $path = parse_url($response->getData()->location, PHP_URL_PATH); - $filename = substr($path, 7); - Queue::assertPushed(ProcessMedia::class); - Storage::disk('local')->assertExists($filename); - // now remove file - unlink(storage_path('app/') . $filename); - } - - public function test_media_endpoint_upload_a_video_file() - { - Queue::fake(); - Storage::fake('s3'); - $file = __DIR__ . '/../video.ogv'; - - $response = $this->post( - '/api/media', - [ - 'file' => new UploadedFile($file, 'video.ogv', 'video/ogg', null, true), - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - - $path = parse_url($response->getData()->location, PHP_URL_PATH); - $filename = substr($path, 7); - Queue::assertPushed(ProcessMedia::class); - Storage::disk('local')->assertExists($filename); - // now remove file - unlink(storage_path('app/') . $filename); - } - - public function test_media_endpoint_upload_a_document_file() - { - Queue::fake(); - Storage::fake('s3'); - - $response = $this->post( - '/api/media', - [ - 'file' => UploadedFile::fake()->create('document.pdf', 100), - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - - $path = parse_url($response->getData()->location, PHP_URL_PATH); - $filename = substr($path, 7); - Queue::assertPushed(ProcessMedia::class); - Storage::disk('local')->assertExists($filename); - // now remove file - unlink(storage_path('app/') . $filename); - } - - public function test_media_endpoint_upload_an_invalid_file_return_error() - { - Queue::fake(); - Storage::fake('local'); - - $response = $this->post( - '/api/media', - [ - 'file' => new UploadedFile( - __DIR__ . '/../aaron.png', - 'aaron.png', - 'image/png', - UPLOAD_ERR_INI_SIZE, - true - ), - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $response->assertStatus(400); - $response->assertJson(['error_description' => 'The uploaded file failed validation']); - } - public function test_access_token_form_encoded() { $faker = \Faker\Factory::create(); diff --git a/tests/Feature/MicropubMediaTest.php b/tests/Feature/MicropubMediaTest.php new file mode 100644 index 00000000..6a8c1496 --- /dev/null +++ b/tests/Feature/MicropubMediaTest.php @@ -0,0 +1,206 @@ +post( + '/api/media', + [ + 'file' => new UploadedFile($file, 'aaron.png', 'image/png', null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + + $lastUploadResponse = $this->get( + '/api/media?q=last', + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $lastUploadResponse->assertJson(['url' => $response->getData()->location]); + + // now remove file + unlink(storage_path('app/') . $filename); + } + + /** @test */ + public function optionsRequestReturnsCorsResponse() + { + $response = $this->options('/api/media'); + + $response->assertStatus(200); + $response->assertHeader('access-control-allow-origin', '*'); + } + + /** @test */ + public function mediaEndpointRequestWithInvalidTokenReturns400Response() + { + $response = $this->post( + '/api/media', + [], + ['HTTP_Authorization' => 'Bearer abc123'] + ); + $response->assertStatus(400); + $response->assertJsonFragment(['error_description' => 'The provided token did not pass validation']); + } + + /** @test */ + public function mediaEndpointRequestWithTokenWithNoScopeReturns400Response() + { + $response = $this->post( + '/api/media', + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithNoScope()] + ); + $response->assertStatus(400); + $response->assertJsonFragment(['error_description' => 'The provided token has no scopes']); + } + + /** @test */ + public function mediaEndpointRequestWithInsufficientTokenScopesReturns401Response() + { + $response = $this->post( + '/api/media', + [], + ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] + ); + $response->assertStatus(401); + $response->assertJsonFragment([ + 'error_description' => 'The token’s scope does not have the necessary requirements.', + ]); + } + + /** @test */ + public function mediaEndpointUploadFile() + { + Queue::fake(); + Storage::fake('s3'); + $file = __DIR__ . '/../aaron.png'; + + $response = $this->post( + '/api/media', + [ + 'file' => new UploadedFile($file, 'aaron.png', 'image/png', null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + // now remove file + unlink(storage_path('app/') . $filename); + } + + /** @test */ + public function mediaEndpointUploadAudioFile() + { + Queue::fake(); + Storage::fake('s3'); + $file = __DIR__ . '/../audio.mp3'; + + $response = $this->post( + '/api/media', + [ + 'file' => new UploadedFile($file, 'audio.mp3', 'audio/mpeg', null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + // now remove file + unlink(storage_path('app/') . $filename); + } + + /** @test */ + public function mediaEndpointUploadVideoFile() + { + Queue::fake(); + Storage::fake('s3'); + $file = __DIR__ . '/../video.ogv'; + + $response = $this->post( + '/api/media', + [ + 'file' => new UploadedFile($file, 'video.ogv', 'video/ogg', null, true), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + // now remove file + unlink(storage_path('app/') . $filename); + } + + /** @test */ + public function mediaEndpointUploadDocumentFile() + { + Queue::fake(); + Storage::fake('s3'); + + $response = $this->post( + '/api/media', + [ + 'file' => UploadedFile::fake()->create('document.pdf', 100), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + + $path = parse_url($response->getData()->location, PHP_URL_PATH); + $filename = substr($path, 7); + Queue::assertPushed(ProcessMedia::class); + Storage::disk('local')->assertExists($filename); + // now remove file + unlink(storage_path('app/') . $filename); + } + + /** @test */ + public function mediaEndpointUploadInvalidFileReturnsError() + { + Queue::fake(); + Storage::fake('local'); + + $response = $this->post( + '/api/media', + [ + 'file' => new UploadedFile( + __DIR__ . '/../aaron.png', + 'aaron.png', + 'image/png', + UPLOAD_ERR_INI_SIZE, + true + ), + ], + ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] + ); + $response->assertStatus(400); + $response->assertJson(['error_description' => 'The uploaded file failed validation']); + } +}