Refactor much of the API testing code.
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiUploadTest.php
1 <?php
2
3 /**
4 * @group Database
5 * @group Destructive
6 */
7
8 /**
9 * n.b. Ensure that you can write to the images/ directory as the
10 * user that will run tests.
11 */
12
13 // Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on.
14 // This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...)
15 // (and in the case of the other Upload tests, this flat out just actually works... )
16
17 // TODO: refactor into several files
18 // TODO: port the other Upload tests, and other API tests to this framework
19
20 /* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */
21 class ApiTestUser {
22 public $username;
23 public $password;
24 public $email;
25 public $groups;
26 public $user;
27
28 function __construct( $username, $realname = 'Real Name', $email = 'sample@sample.com', $groups = array() ) {
29 $this->username = $username;
30 $this->realname = $realname;
31 $this->email = $email;
32 $this->groups = $groups;
33
34 // don't allow user to hardcode or select passwords -- people sometimes run tests
35 // on live wikis. Sometimes we create sysop users in these tests. A sysop user with
36 // a known password would be a Bad Thing.
37 $this->password = User::randomPassword();
38
39 $this->user = User::newFromName( $this->username );
40 $this->user->load();
41
42 // In an ideal world we'd have a new wiki (or mock data store) for every single test.
43 // But for now, we just need to create or update the user with the desired properties.
44 // we particularly need the new password, since we just generated it randomly.
45 // In core MediaWiki, there is no functionality to delete users, so this is the best we can do.
46 if ( !$this->user->getID() ) {
47 // create the user
48 $this->user = User::createNew(
49 $this->username, array(
50 "email" => $this->email,
51 "real_name" => $this->realname
52 )
53 );
54 if ( !$this->user ) {
55 throw new Exception( "error creating user" );
56 }
57 }
58
59 // update the user to use the new random password and other details
60 $this->user->setPassword( $this->password );
61 $this->user->setEmail( $this->email );
62 $this->user->setRealName( $this->realname );
63 // remove all groups, replace with any groups specified
64 foreach ( $this->user->getGroups() as $group ) {
65 $this->user->removeGroup( $group );
66 }
67 if ( count( $this->groups ) ) {
68 foreach ( $this->groups as $group ) {
69 $this->user->addGroup( $group );
70 }
71 }
72 $this->user->saveSettings();
73
74 }
75
76 }
77
78 abstract class ApiTestCase extends MediaWikiTestCase {
79 public static $users;
80
81 function setUp() {
82 global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser;
83
84 $wgMemc = new FakeMemCachedClient();
85 $wgContLang = Language::factory( 'en' );
86 $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' );
87 $wgRequest = new FauxRequest( array() );
88
89 self::$users = array(
90 'sysop' => new ApiTestUser(
91 'Apitestsysop',
92 'Api Test Sysop',
93 'api_test_sysop@sample.com',
94 array( 'sysop' )
95 ),
96 'uploader' => new ApiTestUser(
97 'Apitestuser',
98 'Api Test User',
99 'api_test_user@sample.com',
100 array()
101 )
102 );
103
104 $wgUser = self::$users['sysop']->user;
105
106 }
107
108 protected function doApiRequest( $params, $session = null ) {
109 $_SESSION = isset( $session ) ? $session : array();
110
111 $request = new FauxRequest( $params, true, $_SESSION );
112 $module = new ApiMain( $request, true );
113 $module->execute();
114
115 return array( $module->getResultData(), $request, $_SESSION );
116 }
117
118 /**
119 * Add an edit token to the API request
120 * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the
121 * request, without actually requesting a "real" edit token
122 * @param $params: key-value API params
123 * @param $session: session array
124 */
125 protected function doApiRequestWithToken( $params, $session ) {
126 if ( $session['wsToken'] ) {
127 // add edit token to fake session
128 $session['wsEditToken'] = $session['wsToken'];
129 // add token to request parameters
130 $params['token'] = md5( $session['wsToken'] ) . User::EDIT_TOKEN_SUFFIX;
131 return $this->doApiRequest( $params, $session );
132 } else {
133 throw new Exception( "request data not in right format" );
134 }
135 }
136
137 }
138
139 /**
140 * @group Database
141 * @group Destructive
142 *
143 * This is pretty sucky... needs to be prettified.
144 */
145 class ApiUploadTest extends ApiTestCase {
146 /**
147 * Fixture -- run before every test
148 */
149 public function setUp() {
150 global $wgEnableUploads, $wgEnableAPI;
151 parent::setUp();
152
153 $wgEnableUploads = true;
154 $wgEnableAPI = true;
155 wfSetupSession();
156
157 ini_set( 'log_errors', 1 );
158 ini_set( 'error_reporting', 1 );
159 ini_set( 'display_errors', 1 );
160
161 $this->clearFakeUploads();
162 }
163
164 /**
165 * Fixture -- run after every test
166 * Clean up temporary files etc.
167 */
168 function tearDown() {
169 }
170
171
172 /**
173 * Testing login
174 * XXX this is a funny way of getting session context
175 */
176 function testLogin() {
177 $user = self::$users['uploader'];
178
179 $params = array(
180 'action' => 'login',
181 'lgname' => $user->username,
182 'lgpassword' => $user->password
183 );
184 list( $result, , ) = $this->doApiRequest( $params );
185 $this->assertArrayHasKey( "login", $result );
186 $this->assertArrayHasKey( "result", $result['login'] );
187 $this->assertEquals( "NeedToken", $result['login']['result'] );
188 $token = $result['login']['token'];
189
190 $params = array(
191 'action' => 'login',
192 'lgtoken' => $token,
193 'lgname' => $user->username,
194 'lgpassword' => $user->password
195 );
196 list( $result, , $session ) = $this->doApiRequest( $params );
197 $this->assertArrayHasKey( "login", $result );
198 $this->assertArrayHasKey( "result", $result['login'] );
199 $this->assertEquals( "Success", $result['login']['result'] );
200 $this->assertArrayHasKey( 'lgtoken', $result['login'] );
201
202 return $session;
203
204 }
205
206 /**
207 * @depends testLogin
208 */
209 public function testUploadRequiresToken( $session ) {
210 $exception = false;
211 try {
212 $this->doApiRequest( array(
213 'action' => 'upload'
214 ) );
215 } catch ( UsageException $e ) {
216 $exception = true;
217 $this->assertEquals( "The token parameter must be set", $e->getMessage() );
218 }
219 $this->assertTrue( $exception, "Got exception" );
220 }
221
222 /**
223 * @depends testLogin
224 */
225 public function testUploadMissingParams( $session ) {
226 global $wgUser;
227 $wgUser = self::$users['uploader']->user;
228
229 $exception = false;
230 try {
231 $this->doApiRequestWithToken( array(
232 'action' => 'upload',
233 ), $session );
234 } catch ( UsageException $e ) {
235 $exception = true;
236 $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required",
237 $e->getMessage() );
238 }
239 $this->assertTrue( $exception, "Got exception" );
240 }
241
242
243 /**
244 * @depends testLogin
245 */
246 public function testUpload( $session ) {
247 global $wgUser;
248 $wgUser = self::$users['uploader']->user;
249
250 $extension = 'png';
251 $mimeType = 'image/png';
252
253 try {
254 $randomImageGenerator = new RandomImageGenerator();
255 }
256 catch ( Exception $e ) {
257 $this->markTestIncomplete( $e->getMessage() );
258 }
259
260 $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
261 $filePath = $filePaths[0];
262 $fileSize = filesize( $filePath );
263 $fileName = basename( $filePath );
264
265 $this->deleteFileByFileName( $fileName );
266 $this->deleteFileByContent( $filePath );
267
268
269 if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
270 $this->markTestIncomplete( "Couldn't upload file!\n" );
271 }
272
273 $params = array(
274 'action' => 'upload',
275 'filename' => $fileName,
276 'file' => 'dummy content',
277 'comment' => 'dummy comment',
278 'text' => "This is the page text for $fileName",
279 );
280
281 $exception = false;
282 try {
283 list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
284 } catch ( UsageException $e ) {
285 $exception = true;
286 }
287 $this->assertTrue( isset( $result['upload'] ) );
288 $this->assertEquals( 'Success', $result['upload']['result'] );
289 $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
290 $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
291 $this->assertFalse( $exception );
292
293 // clean up
294 $this->deleteFileByFilename( $fileName );
295 unlink( $filePath );
296 }
297
298
299 /**
300 * @depends testLogin
301 */
302 public function testUploadZeroLength( $session ) {
303 global $wgUser;
304 $wgUser = self::$users['uploader']->user;
305
306 $mimeType = 'image/png';
307
308 $filePath = tempnam( wfTempDir(), "" );
309 $fileName = "apiTestUploadZeroLength.png";
310
311 $this->deleteFileByFileName( $fileName );
312
313 if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
314 $this->markTestIncomplete( "Couldn't upload file!\n" );
315 }
316
317 $params = array(
318 'action' => 'upload',
319 'filename' => $fileName,
320 'file' => 'dummy content',
321 'comment' => 'dummy comment',
322 'text' => "This is the page text for $fileName",
323 );
324
325 $exception = false;
326 try {
327 $this->doApiRequestWithToken( $params, $session );
328 } catch ( UsageException $e ) {
329 $this->assertContains( 'The file you submitted was empty', $e->getMessage() );
330 $exception = true;
331 }
332 $this->assertTrue( $exception );
333
334 // clean up
335 $this->deleteFileByFilename( $fileName );
336 unlink( $filePath );
337 }
338
339
340 /**
341 * @depends testLogin
342 */
343 public function testUploadSameFileName( $session ) {
344 global $wgUser;
345 $wgUser = self::$users['uploader']->user;
346
347 $extension = 'png';
348 $mimeType = 'image/png';
349
350 try {
351 $randomImageGenerator = new RandomImageGenerator();
352 }
353 catch ( Exception $e ) {
354 $this->markTestIncomplete( $e->getMessage() );
355 }
356
357 $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() );
358 // we'll reuse this filename
359 $fileName = basename( $filePaths[0] );
360
361 // clear any other files with the same name
362 $this->deleteFileByFileName( $fileName );
363
364 // we reuse these params
365 $params = array(
366 'action' => 'upload',
367 'filename' => $fileName,
368 'file' => 'dummy content',
369 'comment' => 'dummy comment',
370 'text' => "This is the page text for $fileName",
371 );
372
373 // first upload .... should succeed
374
375 if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) {
376 $this->markTestIncomplete( "Couldn't upload file!\n" );
377 }
378
379 $exception = false;
380 try {
381 list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session );
382 } catch ( UsageException $e ) {
383 $exception = true;
384 }
385 $this->assertTrue( isset( $result['upload'] ) );
386 $this->assertEquals( 'Success', $result['upload']['result'] );
387 $this->assertFalse( $exception );
388
389 // second upload with the same name (but different content)
390
391 if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) {
392 $this->markTestIncomplete( "Couldn't upload file!\n" );
393 }
394
395 $exception = false;
396 try {
397 list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
398 } catch ( UsageException $e ) {
399 $exception = true;
400 }
401 $this->assertTrue( isset( $result['upload'] ) );
402 $this->assertEquals( 'Warning', $result['upload']['result'] );
403 $this->assertTrue( isset( $result['upload']['warnings'] ) );
404 $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) );
405 $this->assertFalse( $exception );
406
407 // clean up
408 $this->deleteFileByFilename( $fileName );
409 unlink( $filePaths[0] );
410 unlink( $filePaths[1] );
411 }
412
413
414 /**
415 * @depends testLogin
416 */
417 public function testUploadSameContent( $session ) {
418 global $wgUser;
419 $wgUser = self::$users['uploader']->user;
420
421 $extension = 'png';
422 $mimeType = 'image/png';
423
424 try {
425 $randomImageGenerator = new RandomImageGenerator();
426 }
427 catch ( Exception $e ) {
428 $this->markTestIncomplete( $e->getMessage() );
429 }
430 $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
431 $fileNames[0] = basename( $filePaths[0] );
432 $fileNames[1] = "SameContentAs" . $fileNames[0];
433
434 // clear any other files with the same name or content
435 $this->deleteFileByContent( $filePaths[0] );
436 $this->deleteFileByFileName( $fileNames[0] );
437 $this->deleteFileByFileName( $fileNames[1] );
438
439 // first upload .... should succeed
440
441 $params = array(
442 'action' => 'upload',
443 'filename' => $fileNames[0],
444 'file' => 'dummy content',
445 'comment' => 'dummy comment',
446 'text' => "This is the page text for " . $fileNames[0],
447 );
448
449 if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) {
450 $this->markTestIncomplete( "Couldn't upload file!\n" );
451 }
452
453 $exception = false;
454 try {
455 list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
456 } catch ( UsageException $e ) {
457 $exception = true;
458 }
459 $this->assertTrue( isset( $result['upload'] ) );
460 $this->assertEquals( 'Success', $result['upload']['result'] );
461 $this->assertFalse( $exception );
462
463
464 // second upload with the same content (but different name)
465
466 if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) {
467 $this->markTestIncomplete( "Couldn't upload file!\n" );
468 }
469
470 $params = array(
471 'action' => 'upload',
472 'filename' => $fileNames[1],
473 'file' => 'dummy content',
474 'comment' => 'dummy comment',
475 'text' => "This is the page text for " . $fileNames[1],
476 );
477
478 $exception = false;
479 try {
480 list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
481 } catch ( UsageException $e ) {
482 $exception = true;
483 }
484 $this->assertTrue( isset( $result['upload'] ) );
485 $this->assertEquals( 'Warning', $result['upload']['result'] );
486 $this->assertTrue( isset( $result['upload']['warnings'] ) );
487 $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) );
488 $this->assertFalse( $exception );
489
490 // clean up
491 $this->deleteFileByFilename( $fileNames[0] );
492 $this->deleteFileByFilename( $fileNames[1] );
493 unlink( $filePaths[0] );
494 }
495
496
497 /**
498 * @depends testLogin
499 */
500 public function testUploadStash( $session ) {
501 global $wgUser;
502 $wgUser = self::$users['uploader']->user;
503
504 $extension = 'png';
505 $mimeType = 'image/png';
506
507 try {
508 $randomImageGenerator = new RandomImageGenerator();
509 }
510 catch ( Exception $e ) {
511 $this->markTestIncomplete( $e->getMessage() );
512 }
513
514 $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
515 $filePath = $filePaths[0];
516 $fileSize = filesize( $filePath );
517 $fileName = basename( $filePath );
518
519 $this->deleteFileByFileName( $fileName );
520 $this->deleteFileByContent( $filePath );
521
522 if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
523 $this->markTestIncomplete( "Couldn't upload file!\n" );
524 }
525
526 $params = array(
527 'action' => 'upload',
528 'stash' => 1,
529 'filename' => $fileName,
530 'file' => 'dummy content',
531 'comment' => 'dummy comment',
532 'text' => "This is the page text for $fileName",
533 );
534
535 $exception = false;
536 try {
537 list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
538 } catch ( UsageException $e ) {
539 $exception = true;
540 }
541 $this->assertFalse( $exception );
542 $this->assertTrue( isset( $result['upload'] ) );
543 $this->assertEquals( 'Success', $result['upload']['result'] );
544 $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
545 $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
546 $this->assertTrue( isset( $result['upload']['sessionkey'] ) );
547 $sessionkey = $result['upload']['sessionkey'];
548
549 // it should be visible from Special:UploadStash
550 // XXX ...but how to test this, with a fake WebRequest with the session?
551
552 // now we should try to release the file from stash
553 $params = array(
554 'action' => 'upload',
555 'sessionkey' => $sessionkey,
556 'filename' => $fileName,
557 'comment' => 'dummy comment',
558 'text' => "This is the page text for $fileName, altered",
559 );
560
561 $this->clearFakeUploads();
562 $exception = false;
563 try {
564 list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
565 } catch ( UsageException $e ) {
566 $exception = true;
567 }
568 $this->assertTrue( isset( $result['upload'] ) );
569 $this->assertEquals( 'Success', $result['upload']['result'] );
570 $this->assertFalse( $exception );
571
572 // clean up
573 $this->deleteFileByFilename( $fileName );
574 unlink( $filePath );
575 }
576
577
578
579 /**
580 * Helper function -- remove files and associated articles by Title
581 * @param $title Title: title to be removed
582 */
583 public function deleteFileByTitle( $title ) {
584 if ( $title->exists() ) {
585 $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) );
586 $noOldArchive = ""; // yes this really needs to be set this way
587 $comment = "removing for test";
588 $restrictDeletedVersions = false;
589 $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions );
590 if ( !$status->isGood() ) {
591 return false;
592 }
593 $article = new Article( $title );
594 $article->doDeleteArticle( "removing for test" );
595
596 // see if it now doesn't exist; reload
597 $title = Title::newFromText( $fileName, NS_FILE );
598 }
599 return ! ( $title && $title instanceof Title && $title->exists() );
600 }
601
602 /**
603 * Helper function -- remove files and associated articles with a particular filename
604 * @param $fileName String: filename to be removed
605 */
606 public function deleteFileByFileName( $fileName ) {
607 return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) );
608 }
609
610
611 /**
612 * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them.
613 * @param $filePath String: path to file on the filesystem
614 */
615 public function deleteFileByContent( $filePath ) {
616 $hash = File::sha1Base36( $filePath );
617 $dupes = RepoGroup::singleton()->findBySha1( $hash );
618 $success = true;
619 foreach ( $dupes as $dupe ) {
620 $success &= $this->deleteFileByTitle( $dupe->getTitle() );
621 }
622 return $success;
623 }
624
625 /**
626 * Fake an upload by dumping the file into temp space, and adding info to $_FILES.
627 * (This is what PHP would normally do).
628 * @param $fieldName String: name this would have in the upload form
629 * @param $fileName String: name to title this
630 * @param $type String: mime type
631 * @param $filePath String: path where to find file contents
632 */
633 function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) {
634 $tmpName = tempnam( wfTempDir(), "" );
635 if ( !file_exists( $filePath ) ) {
636 throw new Exception( "$filePath doesn't exist!" );
637 };
638
639 if ( !copy( $filePath, $tmpName ) ) {
640 throw new Exception( "couldn't copy $filePath to $tmpName" );
641 }
642
643 clearstatcache();
644 $size = filesize( $tmpName );
645 if ( $size === false ) {
646 throw new Exception( "couldn't stat $tmpName" );
647 }
648
649 $_FILES[ $fieldName ] = array(
650 'name' => $fileName,
651 'type' => $type,
652 'tmp_name' => $tmpName,
653 'size' => $size,
654 'error' => null
655 );
656
657 return true;
658
659 }
660
661 /**
662 * Remove traces of previous fake uploads
663 */
664 function clearFakeUploads() {
665 $_FILES = array();
666 }
667
668
669 }
670