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
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');
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With