Reputation: 1097
How can I test a file upload function with a controller test case in CakePHP 3?
I keep running into the problem that PHP thinks the file was not actually uploaded. The validation rules that works for a browser test, but not for a test case:
->add('file', [
'is_uploaded_file' => [
'rule' => ['uploadedFile', ['optional' => false]],
'message' => 'File is no valid uploaded file'
],
I quickly found out that the is_uploaded_file
and move_uploaded_file
are impossible to fool in a unit test.
However, most topics on this are old and/or not about CakePHP specifically, so I figured I'd post a new question.
Upvotes: 1
Views: 1023
Reputation: 60463
You don't necessarily need to modify validation rules, what you can do alternatively is using an object that implements \Psr\Http\Message\UploadedFileInterface
. CakePHP's default uploaded file validation supports such objects.
CakePHP requires zendframework/zend-diactoros
, so you can use \Zend\Diactoros\UploadedFile
and do something like this in your tests:
$data = [
// ...
'file' => new \Zend\Diactoros\UploadedFile([
'/path/to/the/temporary/file/on/disk',
1234, // filesize in bytes
\UPLOAD_ERR_OK, // upload (error) status
'filename.jpg', // upload filename
'image/jpeg' // upload mime type
])
];
The uploadedFile
rule will automatically treat such an object as an uploaded file.
Of course your code that handles the file upload must support that interface too, but it's not that complicated, you just need to make sure that the regular file upload arrays are being converted into UploadedFileInterface
implementations so that your upload handler can make that a requirement.
It could of course be done in the upload handler itself, so that validation will use regular file upload arrays as well as UploadedFile
objects. Another way would be to convert them earlier when creating entities, using the beforeMarshal
handler/event, something along the lines of this:
public function beforeMarshal(\Cake\Event\Event $event, \ArrayObject $data, \ArrayObject $options)
{
$file = \Cake\Utility\Hash::get($data, 'file');
if ($file === null) {
return;
}
if (!($file instanceof \Psr\Http\Message\UploadedFileInterface)) {
if (!is_uploaded_file(\Cake\Utility\Hash::get($file, 'tmp_name'))) {
$file = new \Zend\Diactoros\UploadedFile(
null,
0,
UPLOAD_ERR_NO_FILE,
null,
null
);
} else {
$file = new \Zend\Diactoros\UploadedFile(
\Cake\Utility\Hash::get($file, 'tmp_name'),
\Cake\Utility\Hash::get($file, 'size'),
\Cake\Utility\Hash::get($file, 'error'),
\Cake\Utility\Hash::get($file, 'name'),
\Cake\Utility\Hash::get($file, 'type')
);
}
$data['file'] = $file;
}
}
This will convert the data into an UploadedFile
object in case it's an actually uploaded file. This extra check is added because CakePHP's behavior of merging file data with the POST data, making it impossible (unless one can access the request object, or the $_FILES
superglobal) to determine whether a user posted that data, or whether PHP generated that data for an actual file upload.
If you then use \Psr\Http\Message\UploadedFileInterface::moveTo()
to move the file, it will work in SAPI (browser based) as well as non-SAPI (CLI) environments:
try {
$file->moveTo($targetPath);
} catch (\Exception $exception) {
$entity->setError(
'file', [__('The file could not be moved to its destination.')]
);
}
See also
Upvotes: 2
Reputation: 1097
I actually figured it out almost immediately after I'd posted.
The solution is based on https://pierrerambaud.com/blog/php/2012-12-29-testing-upload-file-with-php
So the only way to get around the problem is overriding both the built-in functions: is_uploaded_file
and move_uploaded_file
.
The uploadedFile
validation rule lives inside Cake\Validation
, and I'm using the move function in a table event, so inside App\Model\Table
.
I added the following to the top of the controller test case:
<?php
namespace Cake\Validation;
function is_uploaded_file($filename)
{
return true;
}
namespace App\Model\Table;
function move_uploaded_file($filename, $destination)
{
return copy($filename, $destination);
}
namespace App\Test\TestCase\Controller;
use App\Controller\CarsController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
use Cake\Core\Configure;
/**
* App\Controller\CarsController Test Case
*/
class CarsControllerTest extends BaseTestCase
{
use IntegrationTestTrait;
// ...
And it works!
Upvotes: 0