Index: branches/uploadwizard/phase3/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php |
— | — | @@ -0,0 +1,289 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/* |
| 5 | + * RandomImageGenerator -- does what it says on the tin. |
| 6 | + * Requires Imagick, the ImageMagick library for PHP, or the command line equivalent (usually 'convert'). |
| 7 | + * |
| 8 | + * Because MediaWiki tests the uniqueness of media upload content, and filenames, it is sometimes useful to generate |
| 9 | + * files that are guaranteed (or at least very likely) to be unique in both those ways. |
| 10 | + * This generates a number of filenames with random names and random content (colored circles) |
| 11 | + * |
| 12 | + * It is also useful to have fresh content because our tests currently run in a "destructive" mode, and don't create a fresh new wiki for each |
| 13 | + * test run. |
| 14 | + * Consequently, if we just had a few static files we kept re-uploading, we'd get lots of warnings about matching content or filenames, |
| 15 | + * and even if we deleted those files, we'd get warnings about archived files. |
| 16 | + * |
| 17 | + * This can also be used with a cronjob to generate random files all the time -- I use it to have a constant, never ending supply when I'm |
| 18 | + * testing interactively. |
| 19 | + * |
| 20 | + * @file |
| 21 | + * @author Neil Kandalgaonkar <neilk@wikimedia.org> |
| 22 | + */ |
| 23 | + |
| 24 | +/** |
| 25 | + * RandomImageGenerator: does what it says on the tin. |
| 26 | + * Can fetch a random image, or also write a number of them to disk with random filenames. |
| 27 | + */ |
| 28 | +class RandomImageGenerator { |
| 29 | + |
| 30 | + private $dictionaryFile; |
| 31 | + private $minWidth = 400; |
| 32 | + private $maxWidth = 800; |
| 33 | + private $minHeight = 400; |
| 34 | + private $maxHeight = 800; |
| 35 | + private $circlesToDraw = 5; |
| 36 | + private $imageWriteMethod; |
| 37 | + |
| 38 | + public function __construct( $options ) { |
| 39 | + global $wgUseImageMagick, $wgImageMagickConvertCommand; |
| 40 | + foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'circlesToDraw' ) as $property ) { |
| 41 | + if ( isset( $options[$property] ) ) { |
| 42 | + $this->$property = $options[$property]; |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + // find the dictionary file, to generate random names |
| 47 | + if ( !isset( $this->dictionaryFile ) ) { |
| 48 | + foreach ( array( '/usr/share/dict/words', '/usr/dict/words' ) as $dictionaryFile ) { |
| 49 | + if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) { |
| 50 | + $this->dictionaryFile = $dictionaryFile; |
| 51 | + break; |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + if ( !isset( $this->dictionaryFile ) ) { |
| 56 | + throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" ); |
| 57 | + } |
| 58 | + |
| 59 | + // figure out how to write images |
| 60 | + if ( class_exists( 'Imagick' ) ) { |
| 61 | + $this->imageWriteMethod = 'writeImageWithApi'; |
| 62 | + } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { |
| 63 | + $this->imageWriteMethod = 'writeImageWithCommandLine'; |
| 64 | + } else { |
| 65 | + throw new Exception( "RandomImageGenerator: could not find a suitable method to write images" ); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Writes random images with random filenames to disk in the directory you specify, or current working directory |
| 71 | + * |
| 72 | + * @param {Integer} number of filenames to write |
| 73 | + * @param {String} format, optional, must be understood by ImageMagick, such as 'jpg' or 'gif' |
| 74 | + * @param {String} directory, optional (will default to current working directory) |
| 75 | + * @return {Array} filenames we just wrote |
| 76 | + */ |
| 77 | + function writeImages( $number, $format = 'jpg', $dir = null ) { |
| 78 | + $filenames = $this->getRandomFilenames( $number, $format, $dir ); |
| 79 | + foreach( $filenames as $filename ) { |
| 80 | + $this->{$this->imageWriteMethod}( $this->getImageSpec(), $format, $filename ); |
| 81 | + } |
| 82 | + return $filenames; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Return a number of randomly-generated filenames |
| 87 | + * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg |
| 88 | + * |
| 89 | + * @param {Integer} number of filenames to generate |
| 90 | + * @param {String} extension, optional, defaults to 'jpg' |
| 91 | + * @param {String} directory, optional, defaults to current working directory |
| 92 | + * @return {Array} of filenames |
| 93 | + */ |
| 94 | + private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) { |
| 95 | + if ( is_null( $dir ) ) { |
| 96 | + $dir = getcwd(); |
| 97 | + } |
| 98 | + $filenames = array(); |
| 99 | + foreach( $this->getRandomWordPairs( $number ) as $pair ) { |
| 100 | + $basename = $pair[0] . '_' . $pair[1]; |
| 101 | + if ( !is_null( $extension ) ) { |
| 102 | + $basename .= '.' . $extension; |
| 103 | + } |
| 104 | + $basename = preg_replace( '/\s+/', '', $basename ); |
| 105 | + $filenames[] = "$dir/$basename"; |
| 106 | + } |
| 107 | + |
| 108 | + return $filenames; |
| 109 | + |
| 110 | + } |
| 111 | + |
| 112 | + |
| 113 | + /** |
| 114 | + * Generate data representing an image of random size (within limits), |
| 115 | + * consisting of randomly colored and sized circles against a random background color |
| 116 | + * (This data is used in the writeImage* methods). |
| 117 | + * @return {Mixed} |
| 118 | + */ |
| 119 | + public function getImageSpec() { |
| 120 | + $spec = array(); |
| 121 | + |
| 122 | + $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth ); |
| 123 | + $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight ); |
| 124 | + $spec['fill'] = $this->getRandomColor(); |
| 125 | + |
| 126 | + $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) ); |
| 127 | + |
| 128 | + $draws = array(); |
| 129 | + for ( $i = 0; $i <= $this->circlesToDraw; $i++ ) { |
| 130 | + $radius = mt_rand( 0, $diagonalLength / 4 ); |
| 131 | + $originX = mt_rand( -1 * $radius, $spec['width'] + $radius ); |
| 132 | + $originY = mt_rand( -1 * $radius, $spec['height'] + $radius ); |
| 133 | + $perimeterX = $originX + $radius; |
| 134 | + $perimeterY = $originY + $radius; |
| 135 | + |
| 136 | + $draw = array(); |
| 137 | + $draw['fill'] = $this->getRandomColor(); |
| 138 | + $draw['circle'] = array( |
| 139 | + 'originX' => $originX, |
| 140 | + 'originY' => $originY, |
| 141 | + 'perimeterX' => $perimeterX, |
| 142 | + 'perimeterY' => $perimeterY |
| 143 | + ); |
| 144 | + $draws[] = $draw; |
| 145 | + |
| 146 | + } |
| 147 | + |
| 148 | + $spec['draws'] = $draws; |
| 149 | + |
| 150 | + return $spec; |
| 151 | + } |
| 152 | + |
| 153 | + |
| 154 | + /** |
| 155 | + * Based on an image specification, write such an image to disk, using Imagick PHP extension |
| 156 | + * @param $spec: spec describing background and circles to draw |
| 157 | + * @param $format: file format to write |
| 158 | + * @param $filename: filename to write to |
| 159 | + */ |
| 160 | + public function writeImageWithApi( $spec, $format, $filename ) { |
| 161 | + $image = new Imagick(); |
| 162 | + $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); |
| 163 | + |
| 164 | + foreach ( $spec['draws'] as $drawSpec ) { |
| 165 | + $draw = new ImagickDraw(); |
| 166 | + $draw->setFillColor( $drawSpec['fill'] ); |
| 167 | + $circle = $drawSpec['circle']; |
| 168 | + $draw->circle( $circle['originX'], $circle['originY'], $circle['perimeterX'], $circle['perimeterY'] ); |
| 169 | + $image->drawImage( $draw ); |
| 170 | + } |
| 171 | + |
| 172 | + $image->setImageFormat( $format ); |
| 173 | + $image->writeImage( $filename ); |
| 174 | + } |
| 175 | + |
| 176 | + |
| 177 | + /** |
| 178 | + * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert'). |
| 179 | + * |
| 180 | + * Sample command line: |
| 181 | + * $ convert -size 100x60 xc:rgb(90,87,45) \ |
| 182 | + * -draw 'fill rgb(12,34,56) circle 41,39 44,57' \ |
| 183 | + * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \ |
| 184 | + * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png |
| 185 | + * |
| 186 | + * @param $spec: spec describing background and circles to draw |
| 187 | + * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi) |
| 188 | + * @param $filename: filename to write to |
| 189 | + */ |
| 190 | + public function writeImageWithCommandLine( $spec, $format, $filename ) { |
| 191 | + global $wgImageMagickConvertCommand; |
| 192 | + $args = array(); |
| 193 | + $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] ); |
| 194 | + $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] ); |
| 195 | + foreach( $spec['draws'] as $draw ) { |
| 196 | + $fill = $draw['fill']; |
| 197 | + $originX = $draw['circle']['originX']; |
| 198 | + $originY = $draw['circle']['originY']; |
| 199 | + $perimeterX = $draw['circle']['perimeterX']; |
| 200 | + $perimeterY = $draw['circle']['perimeterY']; |
| 201 | + $drawCommand = "fill $fill circle $originX,$originY $perimeterX,$perimeterY"; |
| 202 | + $args[] = '-draw ' . wfEscapeShellArg( $drawCommand ); |
| 203 | + } |
| 204 | + $args[] = $filename; |
| 205 | + |
| 206 | + $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args ); |
| 207 | + $output = wfShellExec( $command, $retval ); |
| 208 | + return ( $retval === 0 ); |
| 209 | + } |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | + /** |
| 214 | + * Generate a string of random colors for ImageMagick, like "rgb(12, 37, 98)" |
| 215 | + * |
| 216 | + * @return {String} |
| 217 | + */ |
| 218 | + public function getRandomColor() { |
| 219 | + $components = array(); |
| 220 | + for ($i = 0; $i <= 2; $i++ ) { |
| 221 | + $components[] = mt_rand( 0, 255 ); |
| 222 | + } |
| 223 | + return 'rgb(' . join(', ', $components) . ')'; |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) ); |
| 228 | + * |
| 229 | + * @param {Integer} number of pairs |
| 230 | + * @return {Array} of two-element arrays |
| 231 | + */ |
| 232 | + private function getRandomWordPairs( $number ) { |
| 233 | + $lines = $this->getRandomLines( $number * 2 ); |
| 234 | + // construct pairs of words |
| 235 | + $pairs = array(); |
| 236 | + $count = count( $lines ); |
| 237 | + for( $i = 0; $i < $count; $i += 2 ) { |
| 238 | + $pairs[] = array( $lines[$i], $lines[$i+1] ); |
| 239 | + } |
| 240 | + return $pairs; |
| 241 | + } |
| 242 | + |
| 243 | + |
| 244 | + /** |
| 245 | + * Return N random lines from a file |
| 246 | + * |
| 247 | + * Will throw exception if the file could not be read or if it had fewer lines than requested. |
| 248 | + * |
| 249 | + * @param {Integer} number of lines desired |
| 250 | + * @string {String} path to file |
| 251 | + * @return {Array} of exactly n elements, drawn randomly from lines the file |
| 252 | + */ |
| 253 | + private function getRandomLines( $number_desired ) { |
| 254 | + $filepath = $this->dictionaryFile; |
| 255 | + |
| 256 | + // initialize array of lines |
| 257 | + $lines = array(); |
| 258 | + for ( $i = 0; $i < $number_desired; $i++ ) { |
| 259 | + $lines[] = null; |
| 260 | + } |
| 261 | + |
| 262 | + /* |
| 263 | + * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of |
| 264 | + * a fixed-size array of lines, less and less frequently as it reads the file. |
| 265 | + */ |
| 266 | + $fh = fopen( $filepath, "r" ); |
| 267 | + if ( !$fh ) { |
| 268 | + throw new Exception( "couldn't open $filepath" ); |
| 269 | + } |
| 270 | + $line_number = 0; |
| 271 | + $max_index = $number_desired - 1; |
| 272 | + while( !feof( $fh ) ) { |
| 273 | + $line = fgets( $fh ); |
| 274 | + if ( $line !== false ) { |
| 275 | + $line_number++; |
| 276 | + $line = trim( $line ); |
| 277 | + if ( mt_rand( 0, $line_number ) <= $max_index ) { |
| 278 | + $lines[ mt_rand( 0, $max_index ) ] = $line; |
| 279 | + } |
| 280 | + } |
| 281 | + } |
| 282 | + fclose( $fh ); |
| 283 | + if ( $line_number < $number_desired ) { |
| 284 | + throw new Exception( "not enough lines in $filepath" ); |
| 285 | + } |
| 286 | + |
| 287 | + return $lines; |
| 288 | + } |
| 289 | + |
| 290 | +} |
Property changes on: branches/uploadwizard/phase3/maintenance/tests/phpunit/includes/api/RandomImageGenerator.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 291 | + native |
Index: branches/uploadwizard/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php |
— | — | @@ -0,0 +1,623 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * n.b. Ensure that you can write to the images/ directory as the |
| 6 | + * user that will run tests. |
| 7 | + */ |
| 8 | + |
| 9 | +// Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on. |
| 10 | +// This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...) |
| 11 | +// (and in the case of the other Upload tests, this flat out just actually works... ) |
| 12 | + |
| 13 | +// TODO: refactor into several files |
| 14 | +// TODO: port the other Upload tests, and other API tests to this framework |
| 15 | + |
| 16 | +require_once( dirname( __FILE__ ) . '/RandomImageGenerator.php' ); |
| 17 | + |
| 18 | +abstract class ApiTestCase extends PHPUnit_Framework_TestCase { |
| 19 | + public static $apiUrl; |
| 20 | + public static $users; |
| 21 | + |
| 22 | + function setUp() { |
| 23 | + global $wgServer, $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser; |
| 24 | + |
| 25 | + self::$apiUrl = $wgServer . wfScript( 'api' ); |
| 26 | + |
| 27 | + $wgMemc = new FakeMemCachedClient(); |
| 28 | + $wgContLang = Language::factory( 'en' ); |
| 29 | + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); |
| 30 | + $wgRequest = new FauxRequest( array() ); |
| 31 | + |
| 32 | + self::$users = array( |
| 33 | + 'sysop' => new ApiTestUser( |
| 34 | + 'Apitestsysop', |
| 35 | + 'Api Test Sysop', |
| 36 | + 'testpass', |
| 37 | + 'api_test_sysop@sample.com', |
| 38 | + array( 'sysop' ) |
| 39 | + ), |
| 40 | + 'uploader' => new ApiTestUser( |
| 41 | + 'Apitestuser', |
| 42 | + 'Api Test User', |
| 43 | + 'testpass', |
| 44 | + 'api_test_user@sample.com', |
| 45 | + array() |
| 46 | + ) |
| 47 | + ); |
| 48 | + |
| 49 | + $wgUser = self::$users['sysop']->user; |
| 50 | + |
| 51 | + } |
| 52 | + |
| 53 | + protected function doApiRequest( $params, $session = null ) { |
| 54 | + $_SESSION = isset( $session ) ? $session : array(); |
| 55 | + |
| 56 | + $request = new FauxRequest( $params, true, $_SESSION ); |
| 57 | + $module = new ApiMain( $request, true ); |
| 58 | + $module->execute(); |
| 59 | + |
| 60 | + return array( $module->getResultData(), $request, $_SESSION ); |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Add an edit token to the API request |
| 65 | + * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the |
| 66 | + * request, without actually requesting a "real" edit token |
| 67 | + * @param $params: key-value API params |
| 68 | + * @param $data: a structure which also contains the session |
| 69 | + */ |
| 70 | + protected function doApiRequestWithToken( $params, $session ) { |
| 71 | + if ( $session['wsToken'] ) { |
| 72 | + // add edit token to fake session |
| 73 | + $session['wsEditToken'] = $session['wsToken']; |
| 74 | + // add token to request parameters |
| 75 | + $params['token'] = md5( $session['wsToken'] ) . EDIT_TOKEN_SUFFIX; |
| 76 | + return $this->doApiRequest( $params, $session ); |
| 77 | + } else { |
| 78 | + throw new Exception( "request data not in right format" ); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + function tearDown() { |
| 83 | + global $wgMemc; |
| 84 | + $wgMemc = null; |
| 85 | + } |
| 86 | + |
| 87 | +} |
| 88 | + |
| 89 | +class ApiUploadTest extends ApiTestCase { |
| 90 | + /** |
| 91 | + * Fixture -- run before every test |
| 92 | + */ |
| 93 | + public function setUp() { |
| 94 | + global $wgEnableUploads, $wgEnableAPI, $wgDebugLogFile; |
| 95 | + parent::setUp(); |
| 96 | + |
| 97 | + $wgEnableUploads = true; |
| 98 | + $wgEnableAPI = true; |
| 99 | + wfSetupSession(); |
| 100 | + |
| 101 | + $wgDebugLogFile = '/private/tmp/mwtestdebug.log'; |
| 102 | + ini_set( 'log_errors', 1 ); |
| 103 | + ini_set( 'error_reporting', 1 ); |
| 104 | + ini_set( 'display_errors', 1 ); |
| 105 | + |
| 106 | + $this->clearFakeUploads(); |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Fixture -- run after every test |
| 111 | + * Clean up temporary files etc. |
| 112 | + */ |
| 113 | + function tearDown() { |
| 114 | + } |
| 115 | + |
| 116 | + |
| 117 | + /** |
| 118 | + * Testing login |
| 119 | + * XXX this is a funny way of getting session context |
| 120 | + */ |
| 121 | + function testLogin() { |
| 122 | + $user = self::$users['uploader']; |
| 123 | + $params = array( |
| 124 | + 'action' => 'login', |
| 125 | + 'lgname' => $user->username, |
| 126 | + 'lgpassword' => $user->password |
| 127 | + ); |
| 128 | + list( $result, $request, $session ) = $this->doApiRequest( $params ); |
| 129 | + $this->assertArrayHasKey( "login", $result ); |
| 130 | + $this->assertArrayHasKey( "result", $result['login'] ); |
| 131 | + $this->assertEquals( "NeedToken", $result['login']['result'] ); |
| 132 | + $token = $result['login']['token']; |
| 133 | + |
| 134 | + $params = array( |
| 135 | + 'action' => 'login', |
| 136 | + 'lgtoken' => $token, |
| 137 | + 'lgname' => $user->username, |
| 138 | + 'lgpassword' => $user->password |
| 139 | + ); |
| 140 | + list( $result, $request, $session ) = $this->doApiRequest( $params ); |
| 141 | + $this->assertArrayHasKey( "login", $result ); |
| 142 | + $this->assertArrayHasKey( "result", $result['login'] ); |
| 143 | + $this->assertEquals( "Success", $result['login']['result'] ); |
| 144 | + $this->assertArrayHasKey( 'lgtoken', $result['login'] ); |
| 145 | + |
| 146 | + return $session; |
| 147 | + |
| 148 | + } |
| 149 | + |
| 150 | + /** |
| 151 | + * @depends testLogin |
| 152 | + */ |
| 153 | + public function testUploadRequiresToken( $session ) { |
| 154 | + $exception = false; |
| 155 | + try { |
| 156 | + $this->doApiRequest( array( |
| 157 | + 'action' => 'upload' |
| 158 | + ) ); |
| 159 | + } catch ( UsageException $e ) { |
| 160 | + $exception = true; |
| 161 | + $this->assertEquals( "The token parameter must be set", $e->getMessage() ); |
| 162 | + } |
| 163 | + $this->assertTrue( $exception, "Got exception" ); |
| 164 | + } |
| 165 | + |
| 166 | + /** |
| 167 | + * @depends testLogin |
| 168 | + */ |
| 169 | + public function testUploadMissingParams( $session ) { |
| 170 | + global $wgUser; |
| 171 | + $wgUser = self::$users['uploader']->user; |
| 172 | + |
| 173 | + $exception = false; |
| 174 | + try { |
| 175 | + $this->doApiRequestWithToken( array( |
| 176 | + 'action' => 'upload', |
| 177 | + ), $session ); |
| 178 | + } catch ( UsageException $e ) { |
| 179 | + $exception = true; |
| 180 | + $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", |
| 181 | + $e->getMessage() ); |
| 182 | + } |
| 183 | + $this->assertTrue( $exception, "Got exception" ); |
| 184 | + } |
| 185 | + |
| 186 | + |
| 187 | + /** |
| 188 | + * @depends testLogin |
| 189 | + */ |
| 190 | + public function testUpload( $session ) { |
| 191 | + global $wgUser; |
| 192 | + $wgUser = self::$users['uploader']->user; |
| 193 | + |
| 194 | + $extension = 'png'; |
| 195 | + $mimeType = 'image/png'; |
| 196 | + |
| 197 | + $randomImageGenerator = new RandomImageGenerator(); |
| 198 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 199 | + $filePath = $filePaths[0]; |
| 200 | + $fileName = basename( $filePath ); |
| 201 | + |
| 202 | + $this->deleteFileByFileName( $fileName ); |
| 203 | + $this->deleteFileByContent( $filePath ); |
| 204 | + |
| 205 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 206 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 207 | + } |
| 208 | + |
| 209 | + $params = array( |
| 210 | + 'action' => 'upload', |
| 211 | + 'filename' => $fileName, |
| 212 | + 'file' => 'dummy content', |
| 213 | + 'comment' => 'dummy comment', |
| 214 | + 'text' => "This is the page text for $fileName", |
| 215 | + ); |
| 216 | + |
| 217 | + $exception = false; |
| 218 | + try { |
| 219 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 220 | + } catch ( UsageException $e ) { |
| 221 | + $exception = true; |
| 222 | + } |
| 223 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 224 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 225 | + $this->assertFalse( $exception ); |
| 226 | + |
| 227 | + // clean up |
| 228 | + $this->deleteFileByFilename( $fileName ); |
| 229 | + unlink( $filePath ); |
| 230 | + } |
| 231 | + |
| 232 | + |
| 233 | + /** |
| 234 | + * @depends testLogin |
| 235 | + */ |
| 236 | + public function testUploadZeroLength( $session ) { |
| 237 | + global $wgUser; |
| 238 | + $wgUser = self::$users['uploader']->user; |
| 239 | + |
| 240 | + $extension = 'png'; |
| 241 | + $mimeType = 'image/png'; |
| 242 | + |
| 243 | + $filePath = tempnam( wfTempDir(), "" ); |
| 244 | + $fileName = "apiTestUploadZeroLength.png"; |
| 245 | + |
| 246 | + $this->deleteFileByFileName( $fileName ); |
| 247 | + |
| 248 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 249 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 250 | + } |
| 251 | + |
| 252 | + $params = array( |
| 253 | + 'action' => 'upload', |
| 254 | + 'filename' => $fileName, |
| 255 | + 'file' => 'dummy content', |
| 256 | + 'comment' => 'dummy comment', |
| 257 | + 'text' => "This is the page text for $fileName", |
| 258 | + ); |
| 259 | + |
| 260 | + $exception = false; |
| 261 | + try { |
| 262 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 263 | + } catch ( UsageException $e ) { |
| 264 | + $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); |
| 265 | + $exception = true; |
| 266 | + } |
| 267 | + $this->assertTrue( $exception ); |
| 268 | + |
| 269 | + // clean up |
| 270 | + $this->deleteFileByFilename( $fileName ); |
| 271 | + unlink( $filePath ); |
| 272 | + } |
| 273 | + |
| 274 | + |
| 275 | + /** |
| 276 | + * @depends testLogin |
| 277 | + */ |
| 278 | + public function testUploadSameFileName( $session ) { |
| 279 | + global $wgUser; |
| 280 | + $wgUser = self::$users['uploader']->user; |
| 281 | + |
| 282 | + $extension = 'png'; |
| 283 | + $mimeType = 'image/png'; |
| 284 | + |
| 285 | + $randomImageGenerator = new RandomImageGenerator(); |
| 286 | + $filePaths = $randomImageGenerator->writeImages( 2, $extension, dirname( wfTempDir() ) ); |
| 287 | + // we'll reuse this filename |
| 288 | + $fileName = basename( $filePaths[0] ); |
| 289 | + |
| 290 | + // clear any other files with the same name |
| 291 | + $this->deleteFileByFileName( $fileName ); |
| 292 | + |
| 293 | + // we reuse these params |
| 294 | + $params = array( |
| 295 | + 'action' => 'upload', |
| 296 | + 'filename' => $fileName, |
| 297 | + 'file' => 'dummy content', |
| 298 | + 'comment' => 'dummy comment', |
| 299 | + 'text' => "This is the page text for $fileName", |
| 300 | + ); |
| 301 | + |
| 302 | + // first upload .... should succeed |
| 303 | + |
| 304 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) { |
| 305 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 306 | + } |
| 307 | + |
| 308 | + $exception = false; |
| 309 | + try { |
| 310 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 311 | + } catch ( UsageException $e ) { |
| 312 | + $exception = true; |
| 313 | + } |
| 314 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 315 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 316 | + $this->assertFalse( $exception ); |
| 317 | + |
| 318 | + // second upload with the same name (but different content) |
| 319 | + |
| 320 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) { |
| 321 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 322 | + } |
| 323 | + |
| 324 | + $exception = false; |
| 325 | + try { |
| 326 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 327 | + } catch ( UsageException $e ) { |
| 328 | + $exception = true; |
| 329 | + } |
| 330 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 331 | + $this->assertEquals( 'Warning', $result['upload']['result'] ); |
| 332 | + $this->assertTrue( isset( $result['upload']['warnings'] ) ); |
| 333 | + $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) ); |
| 334 | + $this->assertFalse( $exception ); |
| 335 | + |
| 336 | + // clean up |
| 337 | + $this->deleteFileByFilename( $fileName ); |
| 338 | + unlink( $filePaths[0] ); |
| 339 | + unlink( $filePaths[1] ); |
| 340 | + } |
| 341 | + |
| 342 | + |
| 343 | + /** |
| 344 | + * @depends testLogin |
| 345 | + */ |
| 346 | + public function testUploadSameContent( $session ) { |
| 347 | + global $wgUser; |
| 348 | + $wgUser = self::$users['uploader']->user; |
| 349 | + |
| 350 | + $extension = 'png'; |
| 351 | + $mimeType = 'image/png'; |
| 352 | + |
| 353 | + $randomImageGenerator = new RandomImageGenerator(); |
| 354 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 355 | + $fileNames[0] = basename( $filePaths[0] ); |
| 356 | + $fileNames[1] = "SameContentAs" . $fileNames[0]; |
| 357 | + |
| 358 | + // clear any other files with the same name or content |
| 359 | + $this->deleteFileByContent( $filePaths[0] ); |
| 360 | + $this->deleteFileByFileName( $fileNames[0] ); |
| 361 | + $this->deleteFileByFileName( $fileNames[1] ); |
| 362 | + |
| 363 | + // first upload .... should succeed |
| 364 | + |
| 365 | + $params = array( |
| 366 | + 'action' => 'upload', |
| 367 | + 'filename' => $fileNames[0], |
| 368 | + 'file' => 'dummy content', |
| 369 | + 'comment' => 'dummy comment', |
| 370 | + 'text' => "This is the page text for " . $fileNames[0], |
| 371 | + ); |
| 372 | + |
| 373 | + if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) { |
| 374 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 375 | + } |
| 376 | + |
| 377 | + $exception = false; |
| 378 | + try { |
| 379 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 380 | + } catch ( UsageException $e ) { |
| 381 | + $exception = true; |
| 382 | + } |
| 383 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 384 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 385 | + $this->assertFalse( $exception ); |
| 386 | + |
| 387 | + |
| 388 | + // second upload with the same content (but different name) |
| 389 | + |
| 390 | + if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) { |
| 391 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 392 | + } |
| 393 | + |
| 394 | + $params = array( |
| 395 | + 'action' => 'upload', |
| 396 | + 'filename' => $fileNames[1], |
| 397 | + 'file' => 'dummy content', |
| 398 | + 'comment' => 'dummy comment', |
| 399 | + 'text' => "This is the page text for " . $fileNames[1], |
| 400 | + ); |
| 401 | + |
| 402 | + $exception = false; |
| 403 | + try { |
| 404 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 405 | + } catch ( UsageException $e ) { |
| 406 | + $exception = true; |
| 407 | + } |
| 408 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 409 | + $this->assertEquals( 'Warning', $result['upload']['result'] ); |
| 410 | + $this->assertTrue( isset( $result['upload']['warnings'] ) ); |
| 411 | + $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) ); |
| 412 | + $this->assertFalse( $exception ); |
| 413 | + |
| 414 | + // clean up |
| 415 | + $this->deleteFileByFilename( $fileNames[0] ); |
| 416 | + $this->deleteFileByFilename( $fileNames[1] ); |
| 417 | + unlink( $filePaths[0] ); |
| 418 | + } |
| 419 | + |
| 420 | + |
| 421 | + /** |
| 422 | + * @depends testLogin |
| 423 | + */ |
| 424 | + public function testUploadStash( $session ) { |
| 425 | + global $wgUser; |
| 426 | + $wgUser = self::$users['uploader']->user; |
| 427 | + |
| 428 | + $extension = 'png'; |
| 429 | + $mimeType = 'image/png'; |
| 430 | + |
| 431 | + $randomImageGenerator = new RandomImageGenerator(); |
| 432 | + $filePaths = $randomImageGenerator->writeImages( 1, $extension, dirname( wfTempDir() ) ); |
| 433 | + $filePath = $filePaths[0]; |
| 434 | + $fileName = basename( $filePath ); |
| 435 | + |
| 436 | + $this->deleteFileByFileName( $fileName ); |
| 437 | + $this->deleteFileByContent( $filePath ); |
| 438 | + |
| 439 | + if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { |
| 440 | + $this->markTestIncomplete( "Couldn't upload file!\n" ); |
| 441 | + } |
| 442 | + |
| 443 | + $params = array( |
| 444 | + 'action' => 'upload', |
| 445 | + 'stash' => 1, |
| 446 | + 'filename' => $fileName, |
| 447 | + 'file' => 'dummy content', |
| 448 | + 'comment' => 'dummy comment', |
| 449 | + 'text' => "This is the page text for $fileName", |
| 450 | + ); |
| 451 | + |
| 452 | + $exception = false; |
| 453 | + try { |
| 454 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 455 | + } catch ( UsageException $e ) { |
| 456 | + $exception = true; |
| 457 | + } |
| 458 | + $this->assertFalse( $exception ); |
| 459 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 460 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 461 | + $this->assertTrue( isset( $result['upload']['sessionkey'] ) ); |
| 462 | + $sessionkey = $result['upload']['sessionkey']; |
| 463 | + |
| 464 | + // it should be visible from Special:SessionStash |
| 465 | + // XXX ...but how to test this, with a fake WebRequest with the session? |
| 466 | + |
| 467 | + // now we should try to release the file from stash |
| 468 | + $params = array( |
| 469 | + 'action' => 'upload', |
| 470 | + 'sessionkey' => $sessionkey, |
| 471 | + 'filename' => $fileName, |
| 472 | + 'comment' => 'dummy comment', |
| 473 | + 'text' => "This is the page text for $fileName, altered", |
| 474 | + ); |
| 475 | + |
| 476 | + $this->clearFakeUploads(); |
| 477 | + $exception = false; |
| 478 | + try { |
| 479 | + list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); |
| 480 | + } catch ( UsageException $e ) { |
| 481 | + $exception = true; |
| 482 | + } |
| 483 | + $this->assertTrue( isset( $result['upload'] ) ); |
| 484 | + $this->assertEquals( 'Success', $result['upload']['result'] ); |
| 485 | + $this->assertFalse( $exception ); |
| 486 | + |
| 487 | + // clean up |
| 488 | + $this->deleteFileByFilename( $fileName ); |
| 489 | + unlink( $filePath ); |
| 490 | + } |
| 491 | + |
| 492 | + |
| 493 | + |
| 494 | + /** |
| 495 | + * Helper function -- remove files and associated articles by Title |
| 496 | + * @param {Title} title to be removed |
| 497 | + */ |
| 498 | + public function deleteFileByTitle( $title ) { |
| 499 | + if ( $title->exists() ) { |
| 500 | + $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) ); |
| 501 | + $noOldArchive = ""; // yes this really needs to be set this way |
| 502 | + $comment = "removing for test"; |
| 503 | + $restrictDeletedVersions = false; |
| 504 | + $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions ); |
| 505 | + if ( !$status->isGood() ) { |
| 506 | + return false; |
| 507 | + } |
| 508 | + $article = new Article( $title ); |
| 509 | + $article->doDeleteArticle( "removing for test" ); |
| 510 | + |
| 511 | + // see if it now doesn't exist; reload |
| 512 | + $title = Title::newFromText( $fileName, NS_FILE ); |
| 513 | + } |
| 514 | + return ! ( $title && is_a( $title, 'Title' ) && $title->exists() ); |
| 515 | + } |
| 516 | + |
| 517 | + /** |
| 518 | + * Helper function -- remove files and associated articles with a particular filename |
| 519 | + * @param {String} filename to be removed |
| 520 | + */ |
| 521 | + public function deleteFileByFileName( $fileName ) { |
| 522 | + return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) ); |
| 523 | + } |
| 524 | + |
| 525 | + |
| 526 | + /** |
| 527 | + * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them. |
| 528 | + * @param {String} path to file on the filesystem |
| 529 | + */ |
| 530 | + public function deleteFileByContent( $filePath ) { |
| 531 | + $hash = File::sha1Base36( $filePath ); |
| 532 | + $dupes = RepoGroup::singleton()->findBySha1( $hash ); |
| 533 | + $success = true; |
| 534 | + foreach ( $dupes as $key => $dupe ) { |
| 535 | + $success &= $this->deleteFileByTitle( $dupe->getTitle() ); |
| 536 | + } |
| 537 | + return $success; |
| 538 | + } |
| 539 | + |
| 540 | + /** |
| 541 | + * Fake an upload by dumping the file into temp space, and adding info to $_FILES. |
| 542 | + * (This is what PHP would normally do). |
| 543 | + * @param {String}: fieldname - name this would have in the upload form |
| 544 | + * @param {String}: fileName - name to title this |
| 545 | + * @param {String}: mime type |
| 546 | + * @param {String}: filePath - path where to find file contents |
| 547 | + */ |
| 548 | + function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { |
| 549 | + $tmpName = tempnam( wfTempDir(), "" ); |
| 550 | + if ( !file_exists( $filePath ) ) { |
| 551 | + throw new Exception( "$filePath doesn't exist!" ); |
| 552 | + }; |
| 553 | + |
| 554 | + if ( !copy( $filePath, $tmpName ) ) { |
| 555 | + throw new Exception( "couldn't copy $filePath to $tmpName" ); |
| 556 | + } |
| 557 | + |
| 558 | + clearstatcache(); |
| 559 | + $size = filesize( $tmpName ); |
| 560 | + if ( $size === false ) { |
| 561 | + throw new Exception( "couldn't stat $tmpName" ); |
| 562 | + } |
| 563 | + |
| 564 | + $_FILES[ $fieldName ] = array( |
| 565 | + 'name' => $fileName, |
| 566 | + 'type' => $type, |
| 567 | + 'tmp_name' => $tmpName, |
| 568 | + 'size' => $size, |
| 569 | + 'error' => null |
| 570 | + ); |
| 571 | + |
| 572 | + return true; |
| 573 | + |
| 574 | + } |
| 575 | + |
| 576 | + /** |
| 577 | + * Remove traces of previous fake uploads |
| 578 | + */ |
| 579 | + function clearFakeUploads() { |
| 580 | + $_FILES = array(); |
| 581 | + } |
| 582 | + |
| 583 | + |
| 584 | +} |
| 585 | + |
| 586 | +/* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */ |
| 587 | +class ApiTestUser { |
| 588 | + public $username; |
| 589 | + public $password; |
| 590 | + public $email; |
| 591 | + public $groups; |
| 592 | + public $user; |
| 593 | + |
| 594 | + function __construct( $username, $realname = 'Real Name', $password = 'testpass', $email = 'sample@sample.com', $groups = array() ) { |
| 595 | + $this->username = $username; |
| 596 | + $this->realname = $realname; |
| 597 | + $this->password = $password; |
| 598 | + $this->email = $email; |
| 599 | + $this->groups = $groups; |
| 600 | + |
| 601 | + $this->user = User::newFromName( $this->username ); |
| 602 | + $this->user->load(); |
| 603 | + if ( !$this->user->getID() ) { |
| 604 | + $this->user = User::createNew( |
| 605 | + $this->username, array( |
| 606 | + "email" => $this->email, |
| 607 | + "real_name" => $this->realname |
| 608 | + ) |
| 609 | + ); |
| 610 | + if ( !$this->user ) { |
| 611 | + throw new Exception( "error creating user" ); |
| 612 | + } |
| 613 | + $this->user->setPassword( $this->password ); |
| 614 | + if ( count( $this->groups ) ) { |
| 615 | + foreach ( $this->groups as $group ) { |
| 616 | + $this->user->addGroup( $group ); |
| 617 | + } |
| 618 | + } |
| 619 | + $this->user->saveSettings(); |
| 620 | + } |
| 621 | + } |
| 622 | + |
| 623 | + |
| 624 | + } |
Property changes on: branches/uploadwizard/phase3/maintenance/tests/phpunit/includes/api/ApiUploadTest.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 625 | + native |