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