I'm trying to run this functional test on my laravel controller. I would like to test image processing, but to do so I want to fake image uploading. How do I do this? I found a few examples online but none seem to work for me. Here's what I have:
public function testResizeMethod()
{
$this->prepareCleanDB();
$this->_createAccessableCompany();
$local_file = __DIR__ . '/test-files/large-avatar.jpg';
$uploadedFile = new Symfony\Component\HttpFoundation\File\UploadedFile(
$local_file,
'large-avatar.jpg',
'image/jpeg',
null,
null,
true
);
$values = array(
'company_id' => $this->company->id
);
$response = $this->action(
'POST',
'FileStorageController#store',
$values,
['file' => $uploadedFile]
);
$readable_response = $this->getReadableResponseObject($response);
}
But the controller doesn't get passed this check:
elseif (!Input::hasFile('file'))
{
return Response::error('No file uploaded');
}
So somehow the file isn't passed correctly. How do I go about this?
For anyone else stumbling upon this question, you can nowadays do this:
$response = $this->postJson('/product-import', [
'file' => new \Illuminate\Http\UploadedFile(resource_path('test-files/large-avatar.jpg'), 'large-avatar.jpg', null, null, null, true),
]);
UPDATE
In Laravel 6 the constructor of \Illuminate\Http\UploadedFile Class has 5 parameters instead of 6. This is the new constructor:
/**
* #param string $path The full temporary path to the file
* #param string $originalName The original file name of the uploaded file
* #param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream
* #param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK
* #param bool $test Whether the test mode is active
* Local files are used in test mode hence the code should not enforce HTTP uploads
*
* #throws FileException If file_uploads is disabled
* #throws FileNotFoundException If the file does not exist
*/
public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, $test = false)
{
// ...
}
So the above solution becomes simply:
$response = $this->postJson('/product-import', [
'file' => new \Illuminate\Http\UploadedFile(resource_path('test-files/large-avatar.jpg'), 'large-avatar.jpg', null, null, true),
]);
It works for me.
Docs for CrawlerTrait.html#method_action reads:
Parameters
string $method
string $action
array $wildcards
array $parameters
array $cookies
array $files
array $server
string $content
So I assume the correct call should be
$response = $this->action(
'POST',
'FileStorageController#store',
[],
$values,
[],
['file' => $uploadedFile]
);
unless it requires non-empty wildcards and cookies.
The best and Easiest way : First Import the Necessary things
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
Then make a fake file to upload.
Storage::fake('local');
$file = UploadedFile::fake()->create('file.pdf');
Then make a JSON Data to pass the file. Example
$parameters =[
'institute'=>'Allen Peter Institute',
'total_marks'=>'100',
'aggregate_marks'=>'78',
'percentage'=>'78',
'year'=>'2002',
'qualification_document'=>$file,
];
Then send the Data to your API.
$user = User::where('email','candidate#fakemail.com')->first();
$response = $this->json('post', 'api/user', $parameters, $this->headers($user));
$response->assertStatus(200);
I hope it will work.
With phpunit you can attach a file to a form by using attach() method.
Example from lumen docs:
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->name('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
Here is a full example how to test with custom files. I needed this for parsing CSV files with known format so my files had to had exact formatting and contents. If you need just images or random sized files use $file->fake->image() or create() methods. Those come bundled with Laravel.
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class PanelistImportTest extends TestCase
{
/** #test */
public function user_should_be_able_to_upload_csv_file()
{
// If your route requires authenticated user
$user = Factory('App\User')->create();
$this->actingAs($user);
// Fake any disk here
Storage::fake('local');
$filePath='/tmp/randomstring.csv';
// Create file
file_put_contents($filePath, "HeaderA,HeaderB,HeaderC\n");
$this->postJson('/upload', [
'file' => new UploadedFile($filePath,'test.csv', null, null, null, true),
])->assertStatus(200);
Storage::disk('local')->assertExists('test.csv');
}
}
Here is the controller to go with it:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
public function save(Request $request)
{
$file = $request->file('file');
Storage::disk('local')->putFileAs('', $file, $file->getClientOriginalName());
return response([
'message' => 'uploaded'
], 200);
}
}
Add similar setUp() method into your testcase:
protected function setUp()
{
parent::setUp();
$_FILES = array(
'image' => array(
'name' => 'test.jpg',
'tmp_name' => __DIR__ . '/_files/phpunit-test.jpg',
'type' => 'image/jpeg',
'size' => 499,
'error' => 0
)
);
}
This will spoof your $_FILES global and let Laravel think that there is something uploaded.
Related
Validate an input field of HTML form is a simple operation, as follows:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
public function adminPassword(Request $request)
{
$this->parameters = $request->request->all();
...
$new_password = $this->parameters['new_password'];
$validator = Validation::createValidator();
$violations = $validator->validate($new_password, [
new Assert\Length([
'min' => 4
])
]);
if (0 !== count($violations)) {
...
}
...
}
Can validation request of HTML form file upload (image), can be done by Symfony in the same simple way?
public function logoUpload(Request $request)
{
$file = $request->files->get('logo');
...
}
The requirement is not using Twig, or Symfony 'Form' ('createFormBuilder'), as not done above.
In Symfony, the result of $request->files->get('key') is an UploadedFile or null.
With an UploadedFile you can use your validator with a file constraint as the example below :
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validation;
...
public function validateFile(Request $request): ConstraintViolationListInterface
{
$fileConstraints = new File([
'maxSize' => '64M',
'maxSizeMessage' => 'The file is too big',
'mimeTypes' => ['pdf' => 'application/pdf'],
'mimeTypesMessage' => 'The format is incorrect, only PDF allowed'
]);
$validator = Validation::createValidator();
return $validator->validate($request->files->get('key'), $fileConstraints);
}
The method returns an iterator of constraints.
Please note to use MimeTypes you need to install symfony/mime on your app
I have an endpoint, that allows file upload, everything works fine.
Next thing is to cover the endpoint with proper functional test.
And here's the problem - I can't pass the file to the client making the request.
My test class extends \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase.
static::createClient() method creates an instance of ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client and these Client does not support file uploads.
Beacuse of implementing the Symfony\Contracts\HttpClient\HttpClientInterface which defines public function request(string $method, string $url, array $options = []): ResponseInterface; there's no place for passing files argument.
The allowed options in Client does not support files array.
Internaly it looks like this:
ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client::request passes to the internal kernelBrowser an empty array in place of files params (2nd array): $this->kernelBrowser->request($method, $resolvedUrl, [], [], $server, $options['body'] ?? null)
How do you test endpoints with file upload by extending Base class for functional API tests which is ApiTestCase?
Here's some code, to help you visualize the problem:
ApiResource definition in entity:
/**
* #ApiResource(
* collectionOperations={
* "file_upload"={
* "method"="post",
* "controller"=FileUpload::class,
* "path"="/api/file-upload-endpoint",
* "deserialize"=false,
* "openapi_context"={
* "requestBody"={
* "content"={
* "multipart/form-data"={
* "schema"={
* "type"="object",
* "properties"={
* "file"={
* "type"="string",
* "format"="binary"
* }
* }
* }
* }
* }
* }
* }
* },
* },
* )
*/
Test class (don't mind the instance of UploadedFile, it's just there, to show you, that it cannot be passed anywhere):
<?php
declare(strict_types=1);
namespace App\Tests\Api;
use \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
final class FileUploadTest extends ApiTestCase
{
public function testFileUploadSuccessfully():void
{
$file = new UploadedFile(
TESTS_PROJECT_DIR.'/tests/files/Small_sample_of_jet.jpg',
'Small_sample_of_jet.jpg',
'image/jpeg',
);
static::createClient()->request(
'POST',
'/api/file-upload-endpoint',
[
'headers' => [
'Content-Type' => 'multipart/form-data',
],
],
);
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
}
}
And here is what i'm looking for:
<?php
declare(strict_types=1);
namespace App\Tests\Api;
use \ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
final class FileUploadTest extends ApiTestCase
{
public function testFileUploadSuccessfully():void
{
$file = new UploadedFile(
TESTS_PROJECT_DIR.'/tests/files/Small_sample_of_jet.jpg',
'Small_sample_of_jet.jpg',
'image/jpeg',
);
static::createClient()->request(
'POST',
'/api/file-upload-endpoint',
[
'headers' => [
'Content-Type' => 'multipart/form-data',
],
],
[
'file'=>$file
]
);
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
}
}
When modyfing the vendor itself and passing the files to the Client::request and then to the kernelBrowser in place of 2nd empty array, everything works fine (I'm aware of breaking the contract, that's not the issue here ;)).
I'm thinking if there's missing feature of uploading files in ApiTestCase or I just can't find the solution.
Pls halp!
Api Platform version: 2.5.6
PS: I know i can use different client - test.client
$client = static::$kernel->getContainer()->get('test.client');
which is an instance of Symfony\Bundle\FrameworkBundle\KernelBrowser, the same that is used internally by the Api Platform's Client and that supports files array, but that's not the point of my question. I'd like to know how to do file upload with ApiTestCase.
Since the current latest release of api-platform/core (2.5.8) we are able to pass more parameters to kernelBrowser->request via the extra key. This also now includes files!
Here is a very basic example of testing an image upload (implemented based on the official API Platform documentation):
$file = new UploadedFile(
'path/to/images/my_image.png',
'my_image.png',
'image/png',
);
$response = static::createClient()->request('POST', '/upload_image',
[
'headers' => ['Content-Type' => 'multipart/form-data'],
'extra' => [
'files' => [
'file' => $file,
],
],
],
);
i am trying to build an image upload object and append to a request in Laravel.
I currently have a middleware that intercepts requests and check if there are base64 image strings in it and if there are, it converts it to image object and appends it back to the request.
I have been unsuccessful in appending the converted images back to the request as an instance of Illuminate\Http\UploadedFile i have tried a few things the closest thing i found was UploadedFile::fake()->image('avatar.jpg'); but how can i use my own base64 converted image instead of the generate fake image as looking through the UploadedFile Api i could not come up with a solution.
This is what i have so far
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;
class Base64EncodedImageHandler
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request->has('photos')) {
$validator = Validator::make($request->all(), [
'photos' => 'nullable|array|filled',
'photos.*' => 'required_unless:photos,'.null.'|image',
]);
if ($validator->fails()) {
$images = collect($request->input('photos'))->map(function ($item, $key) {
if (preg_match('/^data:image\/(\w+);base64,/', $item)) {
$data = substr($item, strpos($item, ',') + 1);
$data = base64_decode($data);
// Testing to see if the image decoding worked
// Storage::disk('local')->put("public/test/".uniqid().".png", $data);
// Build image object
return $data;
}
return $item;
});
$request = $request->merge(['photos' => $images]);
}
// Dumping request object here
dd($request);
}
return $next($request);
}
}
Based on the answers i got i was able to achieve my initial objective as so
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;
class Base64EncodedImageHandler
{
/**
* Handle an incoming request.
*
* #author Sayra
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// Add an additional method
$request::macro('setConvertedFiles', function ($files) {
$this->convertedFiles = $files;
return $this;
});
if ($request->has('portrait')) {
$images = collect($request->input('portrait'))->map(function ($item, $key) {
if (preg_match('/^data:image\/(\w+);base64,/', $item)) {
$item = substr($item, strpos($item, ',') + 1);
$image = base64_decode($item);
$path = 'public/test/';
$name = uniqid();
$extension = '.jpg';
// Store converted file
if (Storage::put($path.$name, $image)){
return new UploadedFile(storage_path('app/').$path.$name, $name.$extension, 'image/jpeg');
}
}
})->all();
// Added base64 converted files to request, clear base64 params
$photos = $request->photos? $request->photos : [];
$merge = array_merge($photos, $images);
$request->setConvertedFiles(['photos' => $merge]);
$request->merge(['photos' => $merge]);
$request->merge(['portrait' => []]);
// the isValid() method fails as the file was not a direct upload
dd($request->file('photos.0')->isValid(),$request->file('photos'),$request);
}
return $next($request);
}
}
My current and only issue is that when isValid or is_uploaded_file() is called the image is seen as an invalid image i would like to properly fake the image upload so that the base64 converted images will work and go through the same process as the normal images. Thanks.
You can try with Symfony\Component\HttpFoundation\File\UploadedFile to create image object (you should first save your base64 file somewhere temporary, since UploadedFile requires path). Then in your Base64EncodedImageHandler use trait Illuminate\Http\Concerns\InteractsWithInput.
You will need this method: protected function convertUploadedFiles(array $files) which accepts array of symfony UploadedFile. This will make Illuminate\Http\UploadedFile object. Now when you say $request->merge(['photos' => $images]) your 'photos' will be array of UploadedFile objects which you can manipulate.
I am not sure, but maybe your $request->file() will be still empty. If this is true you can create new request, fill it with all other data from original one, but for files give converted UploadedFile array.
Also check this https://github.com/laravel/framework/issues/10791#issuecomment-213529251
They are using custom FormRequest trait to overwrite ->all() method. You may try the same with ->allFiles() or even overwrite $files property. Keep in mind it accepts FileBag, not array
I have a route that calls a third-party API and returns its response. This route can take an ID as a parameter like so: /my/route/{id}.
When I request /my/route/1 I get a 200 success, but when I do /my/route/2, /my/route/3, /my/route/4, etc I get 500 error.
The funny thing is that I always get the correct response body. So both the 200 and 500 responses are returning the data that I need.
My issue is when getSponsor(..) is triggered here:
<?php
namespace App\Http\Controllers\Matrix;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class SponsorReadingController extends Controller
{
/**
* SponsorReadingController constructor.
*/
public function __construct()
{
$this->cookieJar = app('MatrixCookieJar');
$this->client = app('GuzzleClientForMatrix');
}
public function getSponsor($sponsorId, Request $request)
{
// TODO: Refactor this monstrous function
if (!AuthController::isAlreadyLoggedIn($request)) {
$loginRes = AuthController::loginToMatrixApi($this->cookieJar);
if ($loginRes->getStatusCode() === 200) {
$sessionId = AuthController::getSessionIdFromResponse($loginRes);
} else {
return $loginRes;
}
} else {
$sessionId = null;
AuthController::setAuthCookie(
$this->cookieJar, $request->cookie('matrix_api_session')
);
}
$respData = [
'error' => null,
'message' => null,
'data' => json_decode(
$this->client->get(
'/admin/sponsor/detail',
[
'query' => ['sponsorId' => $sponsorId],
'cookies' => $this->cookieJar,
'allow_redirects' => false
]
)->getBody())->response
];
return $this->handleResponse($respData, $sessionId);
}
/**
* Handle the response with the provided data and
* cookie value.
*
* #param array $respData
* #param string $cookieVal
* #return Response
*/
public function handleResponse($respData, $cookieVal)
{
if (!empty($cookieVal)) {
return response()->json($respData)->withCookie(
cookie('matrix_api_session', $cookieVal, 29, '/matrix/api')
);
}
return response()->json($respData);
}
EDIT: If I do dd($res) instead of return $res inside handleResponse(...) I get a 200 status code, weird.
For reference, this answer helped me out: 500 Internal Server Error for php file not for html
So basically, I added ini_set('display_errors', 1) so the response would include any errors on the back-end that I wasn't seeing (turned out apache's error log had it), and sure enough there was an error that was not directly affecting the response so I would still get the correct response data.
The file were the cookies were being stored couldn't be seen from Guzzle's point of view, but the Laravel app itself could see it. For obvious reasons you need to specify the full path to a folder/file on the server. I ended up using Laravel's own storage folder to store them, so I had to change my service provider from this (where cookies/jar.json used to be in Laravel's public folder:
$this->app->bind('MatrixCookieJar', function ($app) {
return new FileCookieJar(
'cookies/jar.json', true
);
});
To this:
$this->app->singleton('MatrixCookieJar', function ($app) {
return new FileCookieJar(
storage_path('cookies').'/'.'jar.json', true
);
});
I am trying to build a file management system in Laravel based on league/flysystem: https://github.com/thephpleague/flysystem
I am using the S3 adapter and I have it working to save the uploaded files using:
$filesystem->write('filename.txt', 'contents');
Now I am stuck on generating the download file URL when using the S3 adapter.
The files are saved correctly in the S3 bucket, I have permissions to access them, I just don't know how to get to the S3 getObjectUrl method through the league/flysystem package.
I have tried:
$contents = $filesystem->read('filename.txt');
but that returns the content of the file.
$contents = $filemanager->listContents();
or
$paths = $filemanager->listPaths();
but they give me the relative paths to my files.
What I need is something like "ht...//[s3-region].amazonaws.com/[bucket]/[dir]/[file]..."
I am using Laravel 5.2 and the code below seemed to work fine.
Storage::cloud()->url('filename');
I'm not sure what the correct way of doing this is with Flysystem, but the underlying S3Client object has a method for doing that. You could do $filesystem->getAdapter()->getClient()->getObjectUrl($bucket, $key);. Of course, building the URL is as trivial as you described, so you don't really need a special method to do it.
When updating to Laravel 5.1 this method no longer supported by the adapter. No in your config you must have the S3_REGION set or you will get a invalid hostname error and secondly I had to use the command as input to create the presignedRequest.
public function getFilePathAttribute($value)
{
$disk = Storage::disk('s3');
if ($disk->exists($value)) {
$command = $disk->getDriver()->getAdapter()->getClient()->getCommand('GetObject', [
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $value,
'ResponseContentDisposition' => 'attachment;'
]);
$request = $disk->getDriver()->getAdapter()->getClient()->createPresignedRequest($command, '+5 minutes');
return (string) $request->getUri();
}
return $value;
}
Maybe I'm a little late to this question, but here's a way to use Laravel 5's built-in Filesystem.
I created a Manager class that extends Laravel's FilesystemManager to handle the public url retrieval:
class FilesystemPublicUrlManager extends FilesystemManager
{
public function publicUrl($name = null, $object_path = '')
{
$name = $name ?: $this->getDefaultDriver();
$config = $this->getConfig($name);
return $this->{'get' . ucfirst($config['driver']) . 'PublicUrl'}($config, $object_path);
}
public function getLocalPublicUrl($config, $object_path = '')
{
return URL::to('/public') . $object_path;
}
public function getS3PublicUrl($config, $object_path = '')
{
$config += ['version' => 'latest'];
if ($config['key'] && $config['secret']) {
$config['credentials'] = Arr::only($config, ['key', 'secret']);
}
return (new S3Client($config))->getObjectUrl($config['bucket'], $object_path);
}
}
Then, I added this class to the AppServiceProvider under the register method so it has access to the current app instance:
$this->app->singleton('filesystemPublicUrl', function () {
return new FilesystemPublicUrlManager($this->app);
});
Finally, for easy static access, I created a Facade class:
use Illuminate\Support\Facades\Facade;
class StorageUrl extends Facade
{
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor()
{
return 'filesystemPublicUrl';
}
}
Now, I can easily get the public url for my public objects on my local and s3 filesystems (note that I didn't add anything for ftp or rackspace in the FilesystemPublicUrlManager):
$s3Url = StorageUrl::publicUrl('s3') //using the s3 driver
$localUrl = StorageUrl::publicUrl('local') //using the local driver
$defaultUrl = StorageUrl::publicUrl() //default driver
$objectUrl = StorageUrl::publicUrl('s3', '/path/to/object');
Another form of Storage::cloud():
/** #var FilesystemAdapter $disk */
$s3 = Storage::disk('s3');
return $s3->url($path);
Using presigned request S3:
public function getFileUrl($key) {
$s3 = Storage::disk('s3');
$client = $s3->getDriver()->getAdapter()->getClient();
$bucket = env('AWS_BUCKET');
$command = $client->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $key
]);
$request = $client->createPresignedRequest($command, '+20 minutes');
return (string) $request->getUri();
}
For private cloud use this
Storage::disk('s3')->temporaryUrl($path);