Test integration upload Files

Hi,
I use the skeleton create by @maurobonfietti

And now need create a Integration Test for upload files

I see the SlimRequest have the parameter $uploadedFiles I pass the absolute route to this parameter but not work

    public function testUpload()
    {
        $app = $this->getAppInstance();
        $file = __DIR__."/files/pixel.jpg";
        $files = array($file);
        $request = $this->createRequest('POST', '/v1/crm/web/ficheros/upload', 'path=blogs', uploadedFiles:$files);
        $response = $app->handle($request);
        $result = (string) $response->getBody();
        $this->assertEquals(201, $response->getStatusCode());
        $this->assertStringNotContainsString('error', $result);
    }

Some idea?

Thanks

I tried now with

public function testUpload()
{
    $app = $this->getAppInstance();
    $file = __DIR__."/files/pixel.jpg";     

    $uploadedFile = new UploadedFile(
        $file,
        'pixel.jpg',
        'image/jpeg',
        filesize($file),
        0
    );

    $uploadedFiles["files"] = $uploadedFile;

    $request = $this->createRequest('POST', '/v1/crm/web/ficheros/upload', 'path=blogs', ['Content-Type => 'multipart/form-data'], uploadedFiles:$uploadedFiles);
    $response = $app->handle($request);

    $result = (string) $response->getBody();
    $this->assertEquals(201, $response->getStatusCode());
    $this->assertStringNotContainsString('error', $result);
}

And now in the route received this array, but nothing into $_FILES

array(1) {
  ["files"]=>
  object(Slim\Psr7\UploadedFile)#673 (8) {
    ["file":protected]=>
    string(42) "/var/www/tests/integration/files/pixel.jpg"
    ["name":protected]=>
    string(9) "pixel.jpg"
    ["type":protected]=>
    string(10) "image/jpeg"
    ["size":protected]=>
    int(1250)
    ["error":protected]=>
    int(0)
    ["sapi":protected]=>
    bool(false)
    ["stream":protected]=>
    NULL
    ["moved":protected]=>
    bool(false)
  }
}

Some people testing the upload systems?

Thanks for the time.

I have refactored the upload and use the way explained here instead of using the $ _FILES directly

The test now repose 201 but is empty the body, when I upload file with postman I received this

[
    {
        "name": "files.jpg",
        "extension": "jpg",
        "size": 1257,
        "mime": "image/jpeg",
        "md5_file": "4c6e928b41bf75cf10eec2bc83fdd9eb"
    }
]

From what I have seen it seems that it goes up but then when I get to the foreach of the documentation it does nothing

Some help, plis.

Hi @goldrak

Have you tried to add the uploaded files using the withUploadedFiles() method of the response object?

Example:

use Slim\Psr7\Stream;
use Slim\Psr7\UploadedFile;
// ...

$request = $this->createRequest('POST', '/v1/crm/web/ficheros/upload');

// Add header
$request = $request->withHeader('Content-Type', 'multipart/form-data');

// Add files
$stream = new Stream(fopen('php://temp', 'r+'));
$uploadFile = new UploadedFile($stream, 'file.jpg', 'image/jpeg', $stream->getSize());

$request = $request->withUploadedFiles(
    [
        $uploadFile
    ]
);

$response = $app->handle($request);

Hi @odan
hanks for response I tried also with “withUploadedFiles”

This is my base file to phpunit TestCase.php

<?php

declare(strict_types=1);

namespace Tests\integration;

use PHPUnit\Framework\TestCase as PHPUnit_TestCase;
use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Headers;
use Slim\Psr7\Request as SlimRequest;
use Slim\Psr7\Uri;

class TestCase extends PHPUnit_TestCase
{

    protected function getAppInstance()
    {
        require __DIR__ . '/../../src/App/App.php';
        return $app;
    }

    protected function createRequest(
        string $method,
        string $path,
        string $query = '',
        array $headers = ['HTTP_ACCEPT' => 'application/json'],
        array $cookies = [],
        array $serverParams = [],
        array $uploadedFiles = []
    ) {
        $uri = new Uri('', '', 80, $path, $query);
        $handle = fopen('php://temp', 'w+');
        $stream = (new StreamFactory())->createStreamFromResource($handle);

        $h = new Headers();
        foreach ($headers as $name => $value) {
            $h->addHeader($name, $value);
        }
   
        return new SlimRequest($method, $uri, $h, $cookies, $serverParams, $stream, $uploadedFiles);
    }
}

This is my Test file

<?php

declare(strict_types=1);

namespace Tests\integration;

use Slim\Psr7\UploadedFile;

class UploadTest extends TestCase
{
    public function testUpload()
    {
        $app = $this->getAppInstance();
        $file = __DIR__."/files/pixel.jpg";     

        $uploadedFile = new UploadedFile(
            $file,
            'pixel.jpg',
            'image/jpeg',
            filesize($file),
        );
    
        $uploadedFiles["files"] = $uploadedFile;

        $request = $this->createRequest('POST', '/v1/crm/web/ficheros/upload', 'path=blogs');
        $request = $request->withHeader('Content-Type', 'multipart/form-data');
        $request = $request->withUploadedFiles($uploadedFiles);
        $response = $app->handle($request);
     
        $result = (string) $response->getBody();
       
        $this->assertEquals(201, $response->getStatusCode());
        $this->assertStringNotContainsString('error', $result);
    }

}

And this is my controller, it call some other inherit functions but is for call database an calculate de dir need to save the file.

<?php

declare(strict_types=1);

namespace App\Controller\WebFicheros;

use App\Exception\WebFicherosException;
use App\Helper\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\UploadedFileInterface;

final class Upload extends Base
{
    public function __invoke(Request $request, Response $response, array $args): Response
    {

        $parameters = $request->getQueryParams();

        if (isset($parameters['path']) && !empty($parameters['path'])) {
            $path = $parameters['path'];
        } else {
            throw new WebFicherosException('Error path de subida', 400);
        }

        $uri = $request->getUri()->getPath();

       $this->getPathFiles($uri, $path);

        $uploadedFiles = $request->getUploadedFiles();
        $files = [];
        foreach ($uploadedFiles['files'] as $uploadedFile) {
            if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
                $filename = $this->moveUploadedFile($uploadedFile);
                array_push(
                    $files,
                    $this->getFileInfoResponse($filename),
                );
            } else {
                throw new WebFicherosException($this->getErrorUpload($uploadedFile->getError()), 500);
            }
        }
        
        return JsonResponse::withJson($response, $files, 201);
    }

    private function getErrorUpload(int $error) :string 
    {
        $phpFileUploadErrors = array(
            0 => 'There is no error, the file uploaded with success',
            1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
            2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
            3 => 'The uploaded file was only partially uploaded',
            4 => 'No file was uploaded',
            6 => 'Missing a temporary folder',
            7 => 'Failed to write file to disk.',
            8 => 'A PHP extension stopped the file upload.',
        );

        return $phpFileUploadErrors[$error];
    }


    protected function checkMimeType($mime_type, $extension){
        foreach ($this->files_types as $key => $value) {
            if($value['extension'] == $extension && $value['mime_type'] == $mime_type) {
                return true;
            }
        }
        throw new WebFicherosException('Mime type no valido con la extensión del fichero', 500);
    }

    protected function moveUploadedFile(UploadedFileInterface $uploadedFile){
        $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);

        //check mime-type
        $this->checkMimeType($uploadedFile->getClientMediaType(), $extension);

        $name = pathinfo($uploadedFile->getClientFilename(), PATHINFO_FILENAME);
        $name = $this->setNewFileName($name);
        $filename = $this->renameDuplicateFile($name.'.'.$extension);
       
        $uploadedFile->moveTo($this->dir.$filename);
        return $filename;
    }   

    /**
     * Rename the file where it is duplicate with existing file.
     * 
     * @param string $file_name File name to check
     * @return string Return renamed file that will not duplicate the existing file.
     */
    protected function renameDuplicateFile($file_name, $loop_count = 1)
    {
        if (!file_exists($this->dir.$file_name)) {
            return $file_name;
        } else {
            $file_name_explode = explode('.', $file_name);
            $file_extension = (isset($file_name_explode[count($file_name_explode)-1]) ? $file_name_explode[count($file_name_explode)-1] : null);
            unset($file_name_explode[count($file_name_explode)-1]);
            $file_name_only = implode('.', $file_name_explode);
            unset($file_name_explode);

            $i = 1;
            $found = true;
            do {
                $new_file_name = $file_name_only.'_'.$i.'.'.$file_extension;
                if (file_exists($this->dir.$new_file_name)) {
                    $found = true;
                    if ($i > 1000) {
                        // too many loop
                        $file_name = uniqid().'-'.str_replace('.', '', microtime(true));
                        $found = false;
                    }
                } else {
                    $file_name = $new_file_name;
                    $found = false;
                }
                $i++;
            } while ($found === true);

            unset($file_extension, $file_name_only, $new_file_name);
            return $file_name;
        }
    }// renameDuplicateFile

    /**
     * Set the new file name to random. (unique id and microtime).
     */
    protected function setNewFileNameToRandom()
    {
        return uniqid().'-'.str_replace('.', '', microtime(true));
    }// setNewFileNameToRandom


    /**
     * Set the new file name if it was not set, check for reserved file name and removed those characters.
     * 
     * @link http://windows.microsoft.com/en-us/windows/file-names-extensions-faq#1TC=windows-7 Windows file name FAQ.
     * @link https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 Windows reserved file name.
     * @link https://en.wikipedia.org/wiki/Filename Global reserved file name.
     */
    protected function setNewFileName($name)
    {
        $name = trim($name);

        // replace multiple spaces to one space.
        $name = preg_replace('#\s+#iu', ' ', $name);
        // replace space to dash.
        $name = str_replace(' ', '-', $name);
        // replace non alpha-numeric to nothing.
        $name = preg_replace('#[^\da-z\-_]#iu', '', $name);
        // replace multiple dashes to one dash.
        $name = preg_replace('#-{2,}#', '-', $name);

        // do not allow name that contain one of these characters.
        $reserved_characters = array('\\', '/', '?', '%', '*', ':', '|', '"', '<', '>', '!', '@');
        $name = str_replace($reserved_characters, '', $name);
        unset($reserved_characters);

        if (preg_match('#[^\.]+#iu', $name) == 0) {
            // found the name is only dots. example ., .., ..., ....
            return $this->setNewFileNameToRandom();
        }

        // reserved words or reserved names. do not allow if new name is set to one of these words or names.
        // make it case in-sensitive.
        $reserved_words = array(
            'CON', 'PRN', 'AUX', 'CLOCK$', 'NUL', 
            'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 
            'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
            'LST', 'KEYBD$', 'SCREEN$', '$IDLE$', 'CONFIG$',
            '$Mft', '$MftMirr', '$LogFile', '$Volume', '$AttrDef', '$Bitmap', '$Boot', '$BadClus', '$Secure',
            '$Upcase', '$Extend', '$Quota', '$ObjId', '$Reparse',
        );
        foreach ($reserved_words as $reserved_word) {
            if (strtolower($reserved_word) == strtolower($name)) {
                return $this->setNewFileNameToRandom();
            }
        }
        unset($reserved_word, $reserved_words);

        // in the end if it is still left new name as null... set random name to it.
        if ($name == null) {
            return $this->setNewFileNameToRandom();
        }

        return $name;
    }// setNewFileName

}

I don’t kown it with this you kwon what is my problem.

Thanks for your time

The UploadedFile constructor expects a Stream object and not a filename.
Make sure that your filesystem is mocked for testing this here $uploadedFile->moveTo($this->dir.$filename);

I’m sorry but I don’t understand what you mean with this

because I read the constructor of UploadFile and see

 * @param string|StreamInterface $fileNameOrStream The full path to the uploaded file provided by the client or a StreamInterface instance.

I did not know that you could do that, I have created the folder structure with vfsStream

/**
 * @var  vfsStreamDirectory
 */
private $root;

public function setUp(): void
{
    $structure = array(
        'files' => array(
            'private' => array(
                'blogs' => array(),
            ),
            'public' => array(
                'blogs' => array(),
            ),
        )
    );
    
    $this->root   = vfsStream::setup('root', null, $structure);
}

thanks for your time.

My first thought was to use an in-memory stream for testing. Yes, you can also pass a filename. I missed that.