5

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

Jon Skarpeteig
  • 4,118
  • 7
  • 34
  • 53
  • Do you want to **unit** or integration test this? If you want to integration test it: To what extend? Including going through the webserver or not?. The $_REQUEST seems to suggest an integration test but I wanted to ask before answering. – edorian Jan 28 '12 at 19:14
  • I want to unit test the server code recieving uploads, in order to verify that any uploaded file will get treated as expected – Jon Skarpeteig Jan 28 '12 at 19:31
  • Sorry for asking again but I'm not following. You can ether UNIT test this piece of code or test the file upload through the web server, both things kinda don't make sense semantically. If you want to upload a "fake" file that's an integration test. Maybe just the words are the issue here? http://stackoverflow.com/questions/516572/am-i-unit-testing-or-integration-testing – edorian Jan 28 '12 at 19:35
  • I want to unittest it. The rest of the php code is checking if the upload is valid (content_length etc.), and it's checking if it's a chunked upload, and if it's multipart. I want to verify that the code is accurately differentiating the different scenarios, and treat them accordingly - and thus I want to mimic browser behavior (and misbehavior) using fake data through unit tests (not actual browser behavior through integration tests). – Jon Skarpeteig Jan 28 '12 at 19:43

2 Answers2

6

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');
    }
}
Jon Skarpeteig
  • 4,118
  • 7
  • 34
  • 53
  • This is perfect for an integration test. – David Harkness Jan 29 '12 at 19:24
  • Can't seem to make it work. First of all *.phpt test execution needs `--cgi` option, due to `GET` or `POST` usage in test. Also docs. state, that `Currently only available with server-tests.php` and I have no idea where to look for it and how it is connected to my test. – Eugene Aug 06 '13 at 08:35
2

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.

David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • While I agree with your points, which are good advice in general, they don't address my actual question which is about unit testing file uploads. Specifically 'is_uploaded_file', and php://input will fail in my code. It seems to me the only way to test this is using .phpt files, as is_uploaded_file and php://input can't be mocked as far as I have found out. – Jon Skarpeteig Jan 29 '12 at 17:58
  • I added the second-to-last paragraph to hopefully clarify a little. The point is that you won't be calling `is_uploaded_file()` or using `php://input`. Instead mock your own `isUploadedFile()` and `openInputStream()` methods. – David Harkness Jan 29 '12 at 19:19
  • You should not be changing implementation in order to solve a unit test problem in my opinion. :) – Jon Skarpeteig Jan 30 '12 at 13:04
  • I would make all of the above changes without regard for unit tests save one: extracting the call to `is_uploaded_file()`. For that, you have little choice since you cannot mock functions in PHP without an extension. – David Harkness Jan 30 '12 at 14:46