Laravel: Howto test file upload with a real file? - php

I got a controller and a request class to handle a simple file upload.
I am using a test method to test the functioning of the upload. So far so good.
The controller part, which does not get reached (because validation fails (importfile needs to be a file.)), looks like:
public function importCustomers(ImportCustomersRequest $request)
{
dump('importCustomers called!');
The relevant parts of the request class ImportCustomersRequest is this:
public function rules(): array
{
dump('importfile', $this->importfile);
return [
'importfile' => 'required|file',
];
}
The code used for testing is:
$file = UploadedFile::fake()->createWithContent('customer.csv', 'id;name;group\n9999;Customer1;');
// other approachs that did not work:
// $file = UploadedFile::fake()->create('customer.csv')
//
// testing with a real file would be ok too (the file exists!) - but nada (i receive the file content instead of an UploadedFile):
// $file = Storage::path('development/customers.csv')
$response = $this->post(route('import.customers', ['importfile' => $file]));
$response->assertStatus(302);
...
Dumping the importfile value in the request class using the upload via frontend (and a real file) yields a desired
Illuminate\Http\UploadedFile {#2053 ▼
-test: false
-originalName: "customers.csv"
-mimeType: "text/csv"
-error: 0
...
Using the phpunit test from above gives me
array:2 [
"name" => "customer.csv"
"sizeToReport" => "67"
]
which obviously fails.
How am i able to upload a test file (preferably a real file with content) that passes validation (an UploadedFile)?

Related

Using separate data provider class with PHPUnit and attributes

I would like to separate Tests and Data Providers. Using PHP 8 attributes, I cannot get the following test to run when referencing an external Data Provider:
#[Test]
#[DataProviderExternal(RouterDataProvider::class, 'registerGetRouteData')]
public function itRegistersGetRoute(Route $route, array $expectedResult)
{
$this->router->get($route);
$this->assertEquals($expectedResult, $this->router->getRoutes());
}
My data provider class:
class RouterDataProvider
{
public static function registerGetRouteData(): array
{
return [
$route = new Route('/', ['IndexController', 'index']),
[
'GET' => [
'/' => $route,
],
'POST' => []
]
];
}
}
How could I get this test to run with the desired provider method?
By running PHPUnit with the following flags, I was able to see exactly what my issue was:
./vendor/bin/phpunit --display-deprecations --display-warnings --diplay-errors --display-notices
The data set was invalid. Changing the return to yield and updating the return type for the registerGetRouteData method from array to \Generator resolved this.
I was running phpunit with the --testdox flag, so I'm not sure if this is what stopped me seeing any errors initially and assume the test was being skipped.

Uploading a file via PHP + Symfony 4 throws an "File not found error"

I am trying to upload a file using Symfony 4 documentation (https://symfony.com/doc/4.0/controller/upload_file.html) - yes, I know that is an obsolete version, but at the moment I can't update it.
I have done everything as in documentation (except for creating a new entity, because I only need to upload files, and send link to that file in email), files are uploaded correctly to my directory, but it throws HTTP 500 errors, and in log files there are something like this:
[2020-04-20 15:39:40] request.CRITICAL: Uncaught PHP Exception Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException: "The file "/tmp/phpr2tM6D" does not exist" at [...]/vendor/symfony/http-foundation/File/File.php line 37 {"exception":"[object] (Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException(code: 0): The file \"/tmp/phpr2tM6D\" does not exist at [...]/vendor/symfony/http-foundation/File/File.php:37)"} []
any ideas?
ok, so form is basically just
{{ form_start(warranty_form) }}
{{ form_end(warranty_form)}}
and fragments of controller
$form = $this->createForm(WarrantyType::class);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
/** #var UploadedFile $documentFile */
$documentFile = $form->get('documentFile')->getData();
if($documentFile) {
$originalDocumentFilename = pathinfo($documentFile->getClientOriginalName(), PATHINFO_FILENAME);
$newDocumentFilename = uniqid().'.'.$documentFile->guessExtension();
try {
$documentFile->move('%kernel.project_dir%/public/uploads/pdf',$newDocumentFilename);
} catch(FileException $e) {
}
$message = (new \Swift_Message('Test email '))
->setFrom('error#example.org')
->setTo('error#example.org')
->setBody("a","text/plain");
}
and form is just a standard form, with possibility to upload PDF files
->add('documentFile', FileType::class, ['label' => 'Dokument', 'mapped' => false, 'required' => true, 'constraints' => [new File(['maxSize' => '1024k', 'mimeTypes' => ['application/pdf', 'application/x-pdf'], 'mimeTypesMessage' => 'Załaduj prawidłowy dokument'])]])
First i would use your try+catch block to output your error details with
} catch(FileException $e) {
dd($e);
}
There could be more information you need to debug.
From your provided error message "The file "/tmp/phpr2tM6D" does not exist" i would suggest to check that after an upload that this file or a similiar is created for "/tmp" AND way more important is that it can be accessed by your PHP Process. I would say the file is there, but it cannot be read by php. This could happen if your webserver and php use different owner/groups.
Can you show whole controller method? Do you use $documentFile variable after move file?
It looks like the catch(FileException $e) should suppress your exception, and as it still appear I think maybe you somewhere try to access $documentFile variable again or maybe tried to call $form->get('documentFile')->getData()?
You should know that $documentFile->move('', ''); is use $renamed = rename($this->getPathname(), $target);
more see

Lumen does not receive any file from Form Data that was sent

I checked in chrome the Headers send for Form Data which looks something like this:
Now to be that looks like there is an image file named "files" in my POST request.
Now in my Controller in Lumen I do the following debug to try get the file:
return response([$request->hasFile('files'), $request->file('files'), $request->get('files')]);
However what I get is this in the response:
[true,{},null]
This is the request I make in my react app:
const formData = new FormData();
formData.append("files", this.state.productData[key][0]);
fetch(`${process.env.REACT_APP_API_URL}/products/submit`, {
method: 'POST',
body: formData,
})
Does anyone know whats wrong with what I am doing to get the image?
The $request->file() method returns an instance of the Illuminate\Http\UploadedFile class and appears as a {} on your response but you're file is uploaded an is valid when $request->hasFile() is true so you can retrieve its properties like this:
if ($request->hasFile('files')) {
$files = $request->file('files');
return response()->json([
'path' => $files->path(),
'name' => $files->getClientOriginalName(),
'size' => $files->getSize()
]);
}
Or if you want to get response as a file:
if ($request->hasFile('files')) {
return response()->file(
$request->file('files')->path()
);
}
See Laravel docs for retrieving and storing uploaded files.
Note: I recommend to use a name other than files because $request->files is an instance of the Symfony\Component\HttpFoundation\FileBag class and causes a conflict when you want to use $request->files as a shorthand of $request->file('files').

PHP - Calling internal API

I am a newbie in PHP.
I created a Laravel project using *composer**.
My controller has two endpoint uploadFile and testpost:
public function uploadFile(Request $request) {
//there are more code about reading uploaded file here. Everything is OK here.
$request = Request::create('/api/testpost', 'POST',
[],[],[],[],'{"this is" : "my test content"}');
return Route::dispatch($request);
}
public function testpost(Request $request){
Log::info($request->all());
return response()->json(["title"=>"this is the test get method"]);
}
uploadFile is invoked by POST action from a form which carries an uploaded JSON file.
I want to call testpost inside of uploadFile method using Request::create(...) and Route::dispatch(...).
testpost is invoked however the body of request is not as expected. The log file shows me that $request->all() does not return the request body which I expect to be {"this is" : "my test content"}.
My log file:
[2019-02-23 12:16:47] local.INFO: array (
'_token' => 'JzQjclRD4WaTkezqLxlU48D1dM7S3X2X3hok3kr4',
'employee_file' =>
Illuminate\Http\UploadedFile::__set_state(array(
'test' => false,
'originalName' => 'test_input_file.txt',
'mimeType' => 'text/plain',
'error' => 0,
'hashName' => NULL,
)),
)
What wrong in my code? API invocation or request body retrieval?
I know that we can call testpost method directly instead of calling API. However, I ultimate purpose is to know how to call an internal API.
You don't need to use internal API calls for this.
If both methods are in the same class you can invoke directly with
$this->methodName($args);
and it will return the result directly to the calling function. (provided you have a return statement in the method you are invoking)

Simulate a http request and parse route parameters in Laravel testcase

I'm trying to create unit tests to test some specific classes. I use app()->make() to instantiate the classes to test. So actually, no HTTP requests are needed.
However, some of the tested functions need information from the routing parameters so they'll make calls e.g. request()->route()->parameter('info'), and this throws an exception:
Call to a member function parameter() on null.
I've played around a lot and tried something like:
request()->attributes = new \Symfony\Component\HttpFoundation\ParameterBag(['info' => 5]);
request()->route(['info' => 5]);
request()->initialize([], [], ['info' => 5], [], [], [], null);
but none of them worked...
How could I manually initialize the router and feed some routing parameters to it? Or simply make request()->route()->parameter() available?
Update
#Loek: You didn't understand me. Basically, I'm doing:
class SomeTest extends TestCase
{
public function test_info()
{
$info = request()->route()->parameter('info');
$this->assertEquals($info, 'hello_world');
}
}
No "requests" involved. The request()->route()->parameter() call is actually located in a service provider in my real code. This test case is specifically used to test that service provider. There isn't a route which will print the returning value from the methods in that provider.
I assume you need to simulate a request without actually dispatching it. With a simulated request in place, you want to probe it for parameter values and develop your testcase.
There's an undocumented way to do this. You'll be surprised!
The problem
As you already know, Laravel's Illuminate\Http\Request class builds upon Symfony\Component\HttpFoundation\Request. The upstream class does not allow you to setup a request URI manually in a setRequestUri() way. It figures it out based on the actual request headers. No other way around.
OK, enough with the chatter. Let's try to simulate a request:
<?php
use Illuminate\Http\Request;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
dd($request->route()->parameter('info'));
}
}
As you mentioned yourself, you'll get a:
Error: Call to a member function parameter() on null
We need a Route
Why is that? Why route() returns null?
Have a look at its implementation as well as the implementation of its companion method; getRouteResolver(). The getRouteResolver() method returns an empty closure, then route() calls it and so the $route variable will be null. Then it gets returned and thus... the error.
In a real HTTP request context, Laravel sets up its route resolver, so you won't get such errors. Now that you're simulating the request, you need to set up that by yourself. Let's see how.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], ['info' => 5]);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
See another example of creating Routes from Laravel's own RouteCollection class.
Empty parameters bag
So, now you won't get that error because you actually have a route with the request object bound to it. But it won't work yet. If we run phpunit at this point, we'll get a null in the face! If you do a dd($request->route()) you'll see that even though it has the info parameter name set up, its parameters array is empty:
Illuminate\Routing\Route {#250
#uri: "testing/{info}"
#methods: array:2 [
0 => "GET"
1 => "HEAD"
]
#action: array:1 [
"uses" => null
]
#controller: null
#defaults: []
#wheres: []
#parameters: [] <===================== HERE
#parameterNames: array:1 [
0 => "info"
]
#compiled: Symfony\Component\Routing\CompiledRoute {#252
-variables: array:1 [
0 => "info"
]
-tokens: array:2 [
0 => array:4 [
0 => "variable"
1 => "/"
2 => "[^/]++"
3 => "info"
]
1 => array:2 [
0 => "text"
1 => "/testing"
]
]
-staticPrefix: "/testing"
-regex: "#^/testing/(?P<info>[^/]++)$#s"
-pathVariables: array:1 [
0 => "info"
]
-hostVariables: []
-hostRegex: null
-hostTokens: []
}
#router: null
#container: null
}
So passing that ['info' => 5] to Request constructor has no effect whatsoever. Let's have a look at the Route class and see how its $parameters property is getting populated.
When we bind the request object to the route, the $parameters property gets populated by a subsequent call to the bindParameters() method which in turn calls bindPathParameters() to figure out path-specific parameters (we don't have a host parameter in this case).
That method matches request's decoded path against a regex of Symfony's Symfony\Component\Routing\CompiledRoute (You can see that regex in the above dump as well) and returns the matches which are path parameters. It will be empty if the path doesn't match the pattern (which is our case).
/**
* Get the parameter matches for the path portion of the URI.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function bindPathParameters(Request $request)
{
preg_match($this->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
return $matches;
}
The problem is that when there's no actual request, that $request->decodedPath() returns / which does not match the pattern. So the parameters bag will be empty, no matter what.
Spoofing the request URI
If you follow that decodedPath() method on the Request class, you'll go deep through a couple of methods which will finally return a value from prepareRequestUri() of Symfony\Component\HttpFoundation\Request. There, exactly in that method, you'll find the answer to your question.
It's figuring out the request URI by probing a bunch of HTTP headers. It first checks for X_ORIGINAL_URL, then X_REWRITE_URL, then a few others and finally for the REQUEST_URI header. You can set either of these headers to actually spoof the request URI and achieve minimum simulation of a http request. Let's see.
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$request = new Request([], [], [], [], [], ['REQUEST_URI' => 'testing/5']);
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
To your surprise, it prints out 5; the value of info parameter.
Cleanup
You might want to extract the functionality to a helper simulateRequest() method, or a SimulatesRequests trait which can be used across your test cases.
Mocking
Even if it was absolutely impossible to spoof the request URI like the approach above, you could partially mock the request class and set your expected request URI. Something along the lines of:
<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class ExampleTest extends TestCase
{
public function testBasicExample()
{
$requestMock = Mockery::mock(Request::class)
->makePartial()
->shouldReceive('path')
->once()
->andReturn('testing/5');
app()->instance('request', $requestMock->getMock());
$request = request();
$request->setRouteResolver(function () use ($request) {
return (new Route('GET', 'testing/{info}', []))->bind($request);
});
dd($request->route()->parameter('info'));
}
}
This prints out 5 as well.
I ran into this problem today using Laravel7 here is how I solved it, hope it helps somebody
I'm writing unit tests for a middleware, it needs to check for some route parameters, so what I'm doing is creating a fixed request to pass it to the middleware
$request = Request::create('/api/company/{company}', 'GET');
$request->setRouteResolver(function() use ($company) {
$stub = $this->createStub(Route::class);
$stub->expects($this->any())->method('hasParameter')->with('company')->willReturn(true);
$stub->expects($this->any())->method('parameter')->with('company')->willReturn($company->id); // not $adminUser's company
return $stub;
});
Since route is implemented as a closure, you can access a route parameter directly in the route, without explicitly calling parameter('info'). These two calls returns the same:
$info = $request->route()->parameter('info');
$info = $request->route('info');
The second way, makes mocking the 'info' parameter very easy:
$request = $this->createMock(Request::class);
$request->expects($this->once())->method('route')->willReturn('HelloWorld');
$info = $request->route('info');
$this->assertEquals($info, 'HelloWorld');
Of course to exploit this method in your tests, you should inject the Request object in your class under test, instead of using the Laravel global request object through the request() method.
Using the Laravel phpunit wrapper, you can let your test class extend TestCase and use the visit() function.
If you want to be stricter (which in unit testing is probably a good thing), this method isn't really recommended.
class UserTest extends TestCase
{
/**
* A basic test example.
*
* #return void
*/
public function testExample()
{
// This is readable but there's a lot of under-the-hood magic
$this->visit('/home')
->see('Welcome')
->seePageIs('/home');
// You can still be explicit and use phpunit functions
$this->assertTrue(true);
}
}

Categories