Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHPUnit test binary data uploads

How can I populate php://input with binary data in order to test uploads? (Or otherwise test chunked uploads). I am using plupload as my frontend, and I want to unittest my backend.

This is the piece of code I want to test:

public function recieve($file = 'file')
{
    // Get parameters
    $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
    $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
    $fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
    $targetDir = $this->_uploadDir;

    // Clean the fileName for security reasons
    $fileName = preg_replace('/[^\w\._]+/', '_', $fileName);

    // Make sure the fileName is unique but only if chunking is disabled
    if ($chunks < 2 && file_exists($targetDir . DIRECTORY_SEPARATOR . $fileName)) {
        $ext = strrpos($fileName, '.');
        $fileName_a = substr($fileName, 0, $ext);
        $fileName_b = substr($fileName, $ext);

        $count = 1;
        while (file_exists(
                $targetDir . DIRECTORY_SEPARATOR . $fileName_a . '_' . $count . $fileName_b)) {
            $count++;
        }

        $fileName = $fileName_a . '_' . $count . $fileName_b;
    }

    $filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;

    // Create target dir
    if (!file_exists($targetDir)) {
        if (!is_writable(dirname($targetDir))) {
            $this->_messages[] = 'Cannot write to ' . dirname($targetDir) . ' for mkdir';
            return false;
        }
        mkdir($targetDir, 0777, true);
    }

    // Check permissions
    if (!is_writable($targetDir)) {
        $this->_messages[] = 'Unable to write to temp directory.';
        return false;
    }

    // Look for the content type header
    $contentType = null;
    if (isset($_SERVER["HTTP_CONTENT_TYPE"]))
        $contentType = $_SERVER["HTTP_CONTENT_TYPE"];

    if (isset($_SERVER["CONTENT_TYPE"]))
        $contentType = $_SERVER["CONTENT_TYPE"];

    // Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
    if (strpos($contentType, "multipart") !== false) {
        if (isset($_FILES[$file]['tmp_name']) && is_uploaded_file($_FILES[$file]['tmp_name'])) {
            // Open temp file
            $out = fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
            if ($out) {
                // Read binary input stream and append it to temp file
                $in = fopen($_FILES[$file]['tmp_name'], "rb");

                if ($in) {
                    while ($buff = fread($in, 4096)) {
                        fwrite($out, $buff);
                    }
                } else {
                    $this->_messages[] = 'Failed to open input stream.';
                    return false;
                }
                fclose($in);
                fclose($out);
                unlink($_FILES[$file]['tmp_name']);
            } else {
                $this->_messages[] = 'Failed to open output stream.';
                return false;
            }
        } else {
            $this->_messages[] = 'Failed to move uploaded file.';
            return false;
        }
    } else {
        // Open temp file
        $out = fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
        if ($out) {
            // Read binary input stream and append it to temp file
            $in = fopen("php://input", "rb");
            if ($in) {
                while ($buff = fread($in, 4096)) {
                    fwrite($out, $buff);
                }
            } else {
                $this->_messages[] = 'Failed to open input stream.';
                return false;
            }
            fclose($in);
            fclose($out);
        } else {
            $this->_messages[] = 'Failed to open output stream.';
            return false;
        }
    }

    // Check if file upload is complete
    if (!$chunks || $chunk == $chunks - 1) {
        // Strip the temp .part suffix off
        rename("{$filePath}.part", $filePath);
        return $filePath;
    }
}

*Edit:

Added more code, to show what I want to unit test

like image 598
Jon Skarpeteig Avatar asked Jan 28 '12 17:01

Jon Skarpeteig


2 Answers

Seems this can't be done with regular PHPUnit tests, but I found a way to integrate .phpt tests with PHPUnit at: http://qafoo.com/blog/013_testing_file_uploads_with_php.html

For reference, uploadTest.phpt :

--TEST--
Example test emulating a file upload
--POST_RAW--
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfywL8UCjFtqUBTQn

------WebKitFormBoundaryfywL8UCjFtqUBTQn
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain

Contents of text file here

------WebKitFormBoundaryfywL8UCjFtqUBTQn
Content-Disposition: form-data; name="submit"

Upload
------WebKitFormBoundaryfywL8UCjFtqUBTQn--
--FILE--
<?php
require __DIR__ . '/Upload.php';

$upload = new Upload();
$file = $upload->recieve('file');

var_dump(file_exists($file));
?>
--EXPECT--
bool(true)

And corresponding PHPUnit test integration:

<?php
require_once 'PHPUnit/Extensions/PhptTestCase.php';
class UploadExampleTest extends PHPUnit_Extensions_PhptTestCase
{
    public function __construct()
    {
        parent::__construct(__DIR__ . '/uploadTest.phpt');
    }
}
like image 159
Jon Skarpeteig Avatar answered Nov 20 '22 08:11

Jon Skarpeteig


First, you'd find this code significantly easier to unit test if it weren't a single 200 line method! The smaller the unit--the smaller the test. You could extract getFileName(), getContentType(), isChunked() or getChunkDetails(), transferChunk(), etc. Many of these methods would be very short and allow you to test them thoroughly without having to set up an entire upload. Here's one example, getContentType():

public function getContentType() {
    if (isset($_SERVER["CONTENT_TYPE"]))
        return $_SERVER["CONTENT_TYPE"];

    if (isset($_SERVER["HTTP_CONTENT_TYPE"]))
        return $_SERVER["HTTP_CONTENT_TYPE"];

    throw new FileTransferException('Unknown content type');
}

The tests for this method are straight-forward.

/**
 * @expectedException FileTransferException
 */
public function testUnknownContentType() {
    $fixture = new FileTransfer();
    unset($_SERVER["CONTENT_TYPE"]);
    unset($_SERVER["HTTP_CONTENT_TYPE"]);
    $fixture->getContentType();
}

public function testRegularContentType() {
    $fixture = new FileTransfer();
    $_SERVER["CONTENT_TYPE"] = 'regular';
    unset($_SERVER["HTTP_CONTENT_TYPE"]);
    self::assertEquals('regular', $fixture->getContentType());
}

public function testHttpContentType() {
    $fixture = new FileTransfer();
    unset($_SERVER["CONTENT_TYPE"]);
    $_SERVER["HTTP_CONTENT_TYPE"] = 'http';
    self::assertEquals('http', $fixture->getContentType());
}

public function testRegularContentTypeTakesPrecedence() {
    $fixture = new FileTransfer();
    $_SERVER["HTTP_CONTENT_TYPE"] = 'http';
    $_SERVER["CONTENT_TYPE"] = 'regular';
    self::assertEquals('regular', $fixture->getContentType());
}

Once you've refactored the code with the easy stuff, you can extract all of the I/O handling into a separate class. By doing so you can use a mock object when testing the non-I/O code, meaning you won't have to rely on actual files or stuffing php://input with fake data. This is the "unit" part of "unit testing": breaking your code up into small, testable units, and removing the other units from the equation where practical.

In the extracted I/O-handling class, place the calls to is_uploaded_file() and opening the input stream into separate methods, e.g. isUploadedFile() and openInputStream(). While testing you can mock those methods instead of mocking their underlying mechanisms. There's no point in testing that is_uploaded_file() works in a unit test. That's PHP's responsibility, and you can verify everything works as expected in an integration (end-to-end) test.

This will reduce testing your I/O code to the bare minimum. At that point you can use real files in your tests folder or a package like vfsStream.

like image 2
David Harkness Avatar answered Nov 20 '22 08:11

David Harkness