Merge "Fix up logging grouping in ApiStashEdit"
[lhc/web/wiklou.git] / tests / phpunit / includes / RevisionDbTestBase.php
1 <?php
2
3 /**
4 * RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
5 *
6 * @group Database
7 * @group medium
8 */
9 abstract class RevisionDbTestBase extends MediaWikiTestCase {
10
11 /**
12 * @var WikiPage $testPage
13 */
14 private $testPage;
15
16 public function __construct( $name = null, array $data = [], $dataName = '' ) {
17 parent::__construct( $name, $data, $dataName );
18
19 $this->tablesUsed = array_merge( $this->tablesUsed,
20 [
21 'page',
22 'revision',
23 'ip_changes',
24 'text',
25 'archive',
26
27 'recentchanges',
28 'logging',
29
30 'page_props',
31 'pagelinks',
32 'categorylinks',
33 'langlinks',
34 'externallinks',
35 'imagelinks',
36 'templatelinks',
37 'iwlinks'
38 ]
39 );
40 }
41
42 protected function setUp() {
43 global $wgContLang;
44
45 parent::setUp();
46
47 $this->mergeMwGlobalArrayValue(
48 'wgExtraNamespaces',
49 [
50 12312 => 'Dummy',
51 12313 => 'Dummy_talk',
52 ]
53 );
54
55 $this->mergeMwGlobalArrayValue(
56 'wgNamespaceContentModels',
57 [
58 12312 => DummyContentForTesting::MODEL_ID,
59 ]
60 );
61
62 $this->mergeMwGlobalArrayValue(
63 'wgContentHandlers',
64 [
65 DummyContentForTesting::MODEL_ID => 'DummyContentHandlerForTesting',
66 RevisionTestModifyableContent::MODEL_ID => 'RevisionTestModifyableContentHandler',
67 ]
68 );
69
70 $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
71
72 MWNamespace::clearCaches();
73 // Reset namespace cache
74 $wgContLang->resetNamespaces();
75 if ( !$this->testPage ) {
76 /**
77 * We have to create a new page for each subclass as the page creation may result
78 * in different DB fields being filled based on configuration.
79 */
80 $this->testPage = $this->createPage( __CLASS__, __CLASS__ );
81 }
82 }
83
84 protected function tearDown() {
85 global $wgContLang;
86
87 parent::tearDown();
88
89 MWNamespace::clearCaches();
90 // Reset namespace cache
91 $wgContLang->resetNamespaces();
92 }
93
94 abstract protected function getContentHandlerUseDB();
95
96 private function makeRevisionWithProps( $props = null ) {
97 if ( $props === null ) {
98 $props = [];
99 }
100
101 if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
102 $props['text'] = 'Lorem Ipsum';
103 }
104
105 if ( !isset( $props['comment'] ) ) {
106 $props['comment'] = 'just a test';
107 }
108
109 if ( !isset( $props['page'] ) ) {
110 $props['page'] = $this->testPage->getId();
111 }
112
113 $rev = new Revision( $props );
114
115 $dbw = wfGetDB( DB_MASTER );
116 $rev->insertOn( $dbw );
117
118 return $rev;
119 }
120
121 /**
122 * @param string $titleString
123 * @param string $text
124 * @param string|null $model
125 *
126 * @return WikiPage
127 */
128 private function createPage( $titleString, $text, $model = null ) {
129 if ( !preg_match( '/:/', $titleString ) &&
130 ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
131 ) {
132 $ns = $this->getDefaultWikitextNS();
133 $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
134 }
135
136 $title = Title::newFromText( $titleString );
137 $wikipage = new WikiPage( $title );
138
139 // Delete the article if it already exists
140 if ( $wikipage->exists() ) {
141 $wikipage->doDeleteArticle( "done" );
142 }
143
144 $content = ContentHandler::makeContent( $text, $title, $model );
145 $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW );
146
147 return $wikipage;
148 }
149
150 private function assertRevEquals( Revision $orig, Revision $rev = null ) {
151 $this->assertNotNull( $rev, 'missing revision' );
152
153 $this->assertEquals( $orig->getId(), $rev->getId() );
154 $this->assertEquals( $orig->getPage(), $rev->getPage() );
155 $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
156 $this->assertEquals( $orig->getUser(), $rev->getUser() );
157 $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
158 $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
159 $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
160 }
161
162 /**
163 * @covers Revision::insertOn
164 */
165 public function testInsertOn_success() {
166 $parentId = $this->testPage->getLatest();
167
168 // If an ExternalStore is set don't use it.
169 $this->setMwGlobals( 'wgDefaultExternalStore', false );
170
171 $rev = new Revision( [
172 'page' => $this->testPage->getId(),
173 'title' => $this->testPage->getTitle(),
174 'text' => 'Revision Text',
175 'comment' => 'Revision comment',
176 ] );
177
178 $revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
179
180 $this->assertInternalType( 'integer', $revId );
181 $this->assertInternalType( 'integer', $rev->getTextId() );
182 $this->assertSame( $revId, $rev->getId() );
183
184 $this->assertSelect(
185 'text',
186 [ 'old_id', 'old_text' ],
187 "old_id = {$rev->getTextId()}",
188 [ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
189 );
190 $this->assertSelect(
191 'revision',
192 [
193 'rev_id',
194 'rev_page',
195 'rev_text_id',
196 'rev_user',
197 'rev_minor_edit',
198 'rev_deleted',
199 'rev_len',
200 'rev_parent_id',
201 'rev_sha1',
202 ],
203 "rev_id = {$rev->getId()}",
204 [ [
205 strval( $rev->getId() ),
206 strval( $this->testPage->getId() ),
207 strval( $rev->getTextId() ),
208 '0',
209 '0',
210 '0',
211 '13',
212 strval( $parentId ),
213 's0ngbdoxagreuf2vjtuxzwdz64n29xm',
214 ] ]
215 );
216 }
217
218 /**
219 * @covers Revision::insertOn
220 */
221 public function testInsertOn_exceptionOnNoPage() {
222 // If an ExternalStore is set don't use it.
223 $this->setMwGlobals( 'wgDefaultExternalStore', false );
224 $this->setExpectedException(
225 MWException::class,
226 "Cannot insert revision: page ID must be nonzero"
227 );
228
229 $rev = new Revision( [] );
230
231 $rev->insertOn( wfGetDB( DB_MASTER ) );
232 }
233
234 /**
235 * @covers Revision::newFromTitle
236 */
237 public function testNewFromTitle_withoutId() {
238 $latestRevId = $this->testPage->getLatest();
239
240 $rev = Revision::newFromTitle( $this->testPage->getTitle() );
241
242 $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
243 $this->assertEquals( $latestRevId, $rev->getId() );
244 }
245
246 /**
247 * @covers Revision::newFromTitle
248 */
249 public function testNewFromTitle_withId() {
250 $latestRevId = $this->testPage->getLatest();
251
252 $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId );
253
254 $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
255 $this->assertEquals( $latestRevId, $rev->getId() );
256 }
257
258 /**
259 * @covers Revision::newFromTitle
260 */
261 public function testNewFromTitle_withBadId() {
262 $latestRevId = $this->testPage->getLatest();
263
264 $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId + 1 );
265
266 $this->assertNull( $rev );
267 }
268
269 /**
270 * @covers Revision::newFromRow
271 */
272 public function testNewFromRow() {
273 $orig = $this->makeRevisionWithProps();
274
275 $dbr = wfGetDB( DB_REPLICA );
276 $revQuery = Revision::getQueryInfo();
277 $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], [ 'rev_id' => $orig->getId() ],
278 __METHOD__, [], $revQuery['joins'] );
279 $this->assertTrue( is_object( $res ), 'query failed' );
280
281 $row = $res->fetchObject();
282 $res->free();
283
284 $rev = Revision::newFromRow( $row );
285
286 $this->assertRevEquals( $orig, $rev );
287 }
288
289 public function provideNewFromArchiveRow() {
290 yield [
291 function ( $f ) {
292 return $f;
293 },
294 ];
295 yield [
296 function ( $f ) {
297 return $f + [ 'ar_namespace', 'ar_title' ];
298 },
299 ];
300 yield [
301 function ( $f ) {
302 unset( $f['ar_text_id'] );
303 return $f;
304 },
305 ];
306 }
307
308 /**
309 * @dataProvider provideNewFromArchiveRow
310 * @covers Revision::newFromArchiveRow
311 */
312 public function testNewFromArchiveRow( $selectModifier ) {
313 $page = $this->createPage(
314 'RevisionStorageTest_testNewFromArchiveRow',
315 'Lorem Ipsum',
316 CONTENT_MODEL_WIKITEXT
317 );
318 $orig = $page->getRevision();
319 $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
320
321 $dbr = wfGetDB( DB_REPLICA );
322 $arQuery = Revision::getArchiveQueryInfo();
323 $arQuery['fields'] = $selectModifier( $arQuery['fields'] );
324 $res = $dbr->select(
325 $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
326 __METHOD__, [], $arQuery['joins']
327 );
328 $this->assertTrue( is_object( $res ), 'query failed' );
329
330 $row = $res->fetchObject();
331 $res->free();
332
333 $rev = Revision::newFromArchiveRow( $row );
334
335 $this->assertRevEquals( $orig, $rev );
336 }
337
338 /**
339 * @covers Revision::newFromArchiveRow
340 */
341 public function testNewFromArchiveRowOverrides() {
342 $page = $this->createPage(
343 'RevisionStorageTest_testNewFromArchiveRow',
344 'Lorem Ipsum',
345 CONTENT_MODEL_WIKITEXT
346 );
347 $orig = $page->getRevision();
348 $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
349
350 $dbr = wfGetDB( DB_REPLICA );
351 $arQuery = Revision::getArchiveQueryInfo();
352 $res = $dbr->select(
353 $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
354 __METHOD__, [], $arQuery['joins']
355 );
356 $this->assertTrue( is_object( $res ), 'query failed' );
357
358 $row = $res->fetchObject();
359 $res->free();
360
361 $rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
362
363 $this->assertNotEquals( $orig->getComment(), $rev->getComment() );
364 $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
365 }
366
367 /**
368 * @covers Revision::newFromId
369 */
370 public function testNewFromId() {
371 $orig = $this->testPage->getRevision();
372 $rev = Revision::newFromId( $orig->getId() );
373 $this->assertRevEquals( $orig, $rev );
374 }
375
376 /**
377 * @covers Revision::newFromPageId
378 */
379 public function testNewFromPageId() {
380 $rev = Revision::newFromPageId( $this->testPage->getId() );
381 $this->assertRevEquals(
382 $this->testPage->getRevision(),
383 $rev
384 );
385 }
386
387 /**
388 * @covers Revision::newFromPageId
389 */
390 public function testNewFromPageIdWithLatestId() {
391 $rev = Revision::newFromPageId(
392 $this->testPage->getId(),
393 $this->testPage->getLatest()
394 );
395 $this->assertRevEquals(
396 $this->testPage->getRevision(),
397 $rev
398 );
399 }
400
401 /**
402 * @covers Revision::newFromPageId
403 */
404 public function testNewFromPageIdWithNotLatestId() {
405 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
406 $rev = Revision::newFromPageId(
407 $this->testPage->getId(),
408 $this->testPage->getRevision()->getPrevious()->getId()
409 );
410 $this->assertRevEquals(
411 $this->testPage->getRevision()->getPrevious(),
412 $rev
413 );
414 }
415
416 /**
417 * @covers Revision::fetchRevision
418 */
419 public function testFetchRevision() {
420 // Hidden process cache assertion below
421 $this->testPage->getRevision()->getId();
422
423 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
424 $id = $this->testPage->getRevision()->getId();
425
426 $res = Revision::fetchRevision( $this->testPage->getTitle() );
427
428 # note: order is unspecified
429 $rows = [];
430 while ( ( $row = $res->fetchObject() ) ) {
431 $rows[$row->rev_id] = $row;
432 }
433
434 $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
435 $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
436 }
437
438 /**
439 * @covers Revision::getPage
440 */
441 public function testGetPage() {
442 $page = $this->testPage;
443
444 $orig = $this->makeRevisionWithProps( [ 'page' => $page->getId() ] );
445 $rev = Revision::newFromId( $orig->getId() );
446
447 $this->assertEquals( $page->getId(), $rev->getPage() );
448 }
449
450 /**
451 * @covers Revision::isCurrent
452 */
453 public function testIsCurrent() {
454 $rev1 = $this->testPage->getRevision();
455
456 # @todo find out if this should be true
457 # $this->assertTrue( $rev1->isCurrent() );
458
459 $rev1x = Revision::newFromId( $rev1->getId() );
460 $this->assertTrue( $rev1x->isCurrent() );
461
462 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
463 $rev2 = $this->testPage->getRevision();
464
465 # @todo find out if this should be true
466 # $this->assertTrue( $rev2->isCurrent() );
467
468 $rev1x = Revision::newFromId( $rev1->getId() );
469 $this->assertFalse( $rev1x->isCurrent() );
470
471 $rev2x = Revision::newFromId( $rev2->getId() );
472 $this->assertTrue( $rev2x->isCurrent() );
473 }
474
475 /**
476 * @covers Revision::getPrevious
477 */
478 public function testGetPrevious() {
479 $oldestRevision = $this->testPage->getOldestRevision();
480 $latestRevision = $this->testPage->getLatest();
481
482 $this->assertNull( $oldestRevision->getPrevious() );
483
484 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
485 $newRevision = $this->testPage->getRevision();
486
487 $this->assertNotNull( $newRevision->getPrevious() );
488 $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() );
489 }
490
491 /**
492 * @covers Revision::getNext
493 */
494 public function testGetNext() {
495 $rev1 = $this->testPage->getRevision();
496
497 $this->assertNull( $rev1->getNext() );
498
499 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
500 $rev2 = $this->testPage->getRevision();
501
502 $this->assertNotNull( $rev1->getNext() );
503 $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
504 }
505
506 /**
507 * @covers Revision::newNullRevision
508 */
509 public function testNewNullRevision() {
510 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
511 $orig = $this->testPage->getRevision();
512
513 $dbw = wfGetDB( DB_MASTER );
514 $rev = Revision::newNullRevision( $dbw, $this->testPage->getId(), 'a null revision', false );
515
516 $this->assertNotEquals( $orig->getId(), $rev->getId(),
517 'new null revision should have a different id from the original revision' );
518 $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
519 'new null revision should have the same text id as the original revision' );
520 $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
521 }
522
523 /**
524 * @covers Revision::insertOn
525 */
526 public function testInsertOn() {
527 $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
528
529 $orig = $this->makeRevisionWithProps( [
530 'user_text' => $ip
531 ] );
532
533 // Make sure the revision was copied to ip_changes
534 $dbr = wfGetDB( DB_REPLICA );
535 $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
536 $row = $res->fetchObject();
537
538 $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
539 $this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp );
540 }
541
542 public static function provideUserWasLastToEdit() {
543 yield 'actually the last edit' => [ 3, true ];
544 yield 'not the current edit, but still by this user' => [ 2, true ];
545 yield 'edit by another user' => [ 1, false ];
546 yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ];
547 }
548
549 /**
550 * @dataProvider provideUserWasLastToEdit
551 */
552 public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
553 $userA = User::newFromName( "RevisionStorageTest_userA" );
554 $userB = User::newFromName( "RevisionStorageTest_userB" );
555
556 if ( $userA->getId() === 0 ) {
557 $userA = User::createNew( $userA->getName() );
558 }
559
560 if ( $userB->getId() === 0 ) {
561 $userB = User::createNew( $userB->getName() );
562 }
563
564 $ns = $this->getDefaultWikitextNS();
565
566 $dbw = wfGetDB( DB_MASTER );
567 $revisions = [];
568
569 // create revisions -----------------------------
570 $page = WikiPage::factory( Title::newFromText(
571 'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
572 $page->insertOn( $dbw );
573
574 $revisions[0] = new Revision( [
575 'page' => $page->getId(),
576 // we need the title to determine the page's default content model
577 'title' => $page->getTitle(),
578 'timestamp' => '20120101000000',
579 'user' => $userA->getId(),
580 'text' => 'zero',
581 'content_model' => CONTENT_MODEL_WIKITEXT,
582 'summary' => 'edit zero'
583 ] );
584 $revisions[0]->insertOn( $dbw );
585
586 $revisions[1] = new Revision( [
587 'page' => $page->getId(),
588 // still need the title, because $page->getId() is 0 (there's no entry in the page table)
589 'title' => $page->getTitle(),
590 'timestamp' => '20120101000100',
591 'user' => $userA->getId(),
592 'text' => 'one',
593 'content_model' => CONTENT_MODEL_WIKITEXT,
594 'summary' => 'edit one'
595 ] );
596 $revisions[1]->insertOn( $dbw );
597
598 $revisions[2] = new Revision( [
599 'page' => $page->getId(),
600 'title' => $page->getTitle(),
601 'timestamp' => '20120101000200',
602 'user' => $userB->getId(),
603 'text' => 'two',
604 'content_model' => CONTENT_MODEL_WIKITEXT,
605 'summary' => 'edit two'
606 ] );
607 $revisions[2]->insertOn( $dbw );
608
609 $revisions[3] = new Revision( [
610 'page' => $page->getId(),
611 'title' => $page->getTitle(),
612 'timestamp' => '20120101000300',
613 'user' => $userA->getId(),
614 'text' => 'three',
615 'content_model' => CONTENT_MODEL_WIKITEXT,
616 'summary' => 'edit three'
617 ] );
618 $revisions[3]->insertOn( $dbw );
619
620 $revisions[4] = new Revision( [
621 'page' => $page->getId(),
622 'title' => $page->getTitle(),
623 'timestamp' => '20120101000200',
624 'user' => $userA->getId(),
625 'text' => 'zero',
626 'content_model' => CONTENT_MODEL_WIKITEXT,
627 'summary' => 'edit four'
628 ] );
629 $revisions[4]->insertOn( $dbw );
630
631 // test it ---------------------------------
632 $since = $revisions[$sinceIdx]->getTimestamp();
633
634 $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
635
636 $this->assertEquals( $expectedLast, $wasLast );
637 }
638
639 /**
640 * @param string $text
641 * @param string $title
642 * @param string $model
643 * @param string $format
644 *
645 * @return Revision
646 */
647 private function newTestRevision( $text, $title = "Test",
648 $model = CONTENT_MODEL_WIKITEXT, $format = null
649 ) {
650 if ( is_string( $title ) ) {
651 $title = Title::newFromText( $title );
652 }
653
654 $content = ContentHandler::makeContent( $text, $title, $model, $format );
655
656 $rev = new Revision(
657 [
658 'id' => 42,
659 'page' => 23,
660 'title' => $title,
661
662 'content' => $content,
663 'length' => $content->getSize(),
664 'comment' => "testing",
665 'minor_edit' => false,
666
667 'content_format' => $format,
668 ]
669 );
670
671 return $rev;
672 }
673
674 public function provideGetContentModel() {
675 // NOTE: we expect the help namespace to always contain wikitext
676 return [
677 [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ],
678 [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ],
679 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
680 ];
681 }
682
683 /**
684 * @dataProvider provideGetContentModel
685 * @covers Revision::getContentModel
686 */
687 public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
688 $rev = $this->newTestRevision( $text, $title, $model, $format );
689
690 $this->assertEquals( $expectedModel, $rev->getContentModel() );
691 }
692
693 public function provideGetContentFormat() {
694 // NOTE: we expect the help namespace to always contain wikitext
695 return [
696 [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ],
697 [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ],
698 [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ],
699 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
700 ];
701 }
702
703 /**
704 * @dataProvider provideGetContentFormat
705 * @covers Revision::getContentFormat
706 */
707 public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
708 $rev = $this->newTestRevision( $text, $title, $model, $format );
709
710 $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
711 }
712
713 public function provideGetContentHandler() {
714 // NOTE: we expect the help namespace to always contain wikitext
715 return [
716 [ 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ],
717 [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ],
718 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ],
719 ];
720 }
721
722 /**
723 * @dataProvider provideGetContentHandler
724 * @covers Revision::getContentHandler
725 */
726 public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
727 $rev = $this->newTestRevision( $text, $title, $model, $format );
728
729 $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
730 }
731
732 public function provideGetContent() {
733 // NOTE: we expect the help namespace to always contain wikitext
734 return [
735 [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ],
736 [
737 serialize( 'hello world' ),
738 'Hello',
739 DummyContentForTesting::MODEL_ID,
740 null,
741 Revision::FOR_PUBLIC,
742 serialize( 'hello world' )
743 ],
744 [
745 serialize( 'hello world' ),
746 'Dummy:Hello',
747 null,
748 null,
749 Revision::FOR_PUBLIC,
750 serialize( 'hello world' )
751 ],
752 ];
753 }
754
755 /**
756 * @dataProvider provideGetContent
757 * @covers Revision::getContent
758 */
759 public function testGetContent( $text, $title, $model, $format,
760 $audience, $expectedSerialization
761 ) {
762 $rev = $this->newTestRevision( $text, $title, $model, $format );
763 $content = $rev->getContent( $audience );
764
765 $this->assertEquals(
766 $expectedSerialization,
767 is_null( $content ) ? null : $content->serialize( $format )
768 );
769 }
770
771 /**
772 * @covers Revision::getContent
773 */
774 public function testGetContent_failure() {
775 $rev = new Revision( [
776 'page' => $this->testPage->getId(),
777 'content_model' => $this->testPage->getContentModel(),
778 'text_id' => 123456789, // not in the test DB
779 ] );
780
781 $this->assertNull( $rev->getContent(),
782 "getContent() should return null if the revision's text blob could not be loaded." );
783
784 // NOTE: check this twice, once for lazy initialization, and once with the cached value.
785 $this->assertNull( $rev->getContent(),
786 "getContent() should return null if the revision's text blob could not be loaded." );
787 }
788
789 public function provideGetSize() {
790 return [
791 [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ],
792 [ serialize( "hello world." ), DummyContentForTesting::MODEL_ID, 12 ],
793 ];
794 }
795
796 /**
797 * @covers Revision::getSize
798 * @dataProvider provideGetSize
799 */
800 public function testGetSize( $text, $model, $expected_size ) {
801 $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
802 $this->assertEquals( $expected_size, $rev->getSize() );
803 }
804
805 public function provideGetSha1() {
806 return [
807 [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ],
808 [
809 serialize( "hello world." ),
810 DummyContentForTesting::MODEL_ID,
811 Revision::base36Sha1( serialize( "hello world." ) )
812 ],
813 ];
814 }
815
816 /**
817 * @covers Revision::getSha1
818 * @dataProvider provideGetSha1
819 */
820 public function testGetSha1( $text, $model, $expected_hash ) {
821 $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
822 $this->assertEquals( $expected_hash, $rev->getSha1() );
823 }
824
825 /**
826 * Tests whether $rev->getContent() returns a clone when needed.
827 *
828 * @covers Revision::getContent
829 */
830 public function testGetContentClone() {
831 $content = new RevisionTestModifyableContent( "foo" );
832
833 $rev = new Revision(
834 [
835 'id' => 42,
836 'page' => 23,
837 'title' => Title::newFromText( "testGetContentClone_dummy" ),
838
839 'content' => $content,
840 'length' => $content->getSize(),
841 'comment' => "testing",
842 'minor_edit' => false,
843 ]
844 );
845
846 /** @var RevisionTestModifyableContent $content */
847 $content = $rev->getContent( Revision::RAW );
848 $content->setText( "bar" );
849
850 /** @var RevisionTestModifyableContent $content2 */
851 $content2 = $rev->getContent( Revision::RAW );
852 // content is mutable, expect clone
853 $this->assertNotSame( $content, $content2, "expected a clone" );
854 // clone should contain the original text
855 $this->assertEquals( "foo", $content2->getText() );
856
857 $content2->setText( "bla bla" );
858 // clones should be independent
859 $this->assertEquals( "bar", $content->getText() );
860 }
861
862 /**
863 * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
864 * @covers Revision::getContent
865 */
866 public function testGetContentUncloned() {
867 $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
868 $content = $rev->getContent( Revision::RAW );
869 $content2 = $rev->getContent( Revision::RAW );
870
871 // for immutable content like wikitext, this should be the same object
872 $this->assertSame( $content, $content2 );
873 }
874
875 /**
876 * @covers Revision::loadFromId
877 */
878 public function testLoadFromId() {
879 $rev = $this->testPage->getRevision();
880 $this->assertRevEquals(
881 $rev,
882 Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
883 );
884 }
885
886 /**
887 * @covers Revision::loadFromPageId
888 */
889 public function testLoadFromPageId() {
890 $this->assertRevEquals(
891 $this->testPage->getRevision(),
892 Revision::loadFromPageId( wfGetDB( DB_MASTER ), $this->testPage->getId() )
893 );
894 }
895
896 /**
897 * @covers Revision::loadFromPageId
898 */
899 public function testLoadFromPageIdWithLatestRevId() {
900 $this->assertRevEquals(
901 $this->testPage->getRevision(),
902 Revision::loadFromPageId(
903 wfGetDB( DB_MASTER ),
904 $this->testPage->getId(),
905 $this->testPage->getLatest()
906 )
907 );
908 }
909
910 /**
911 * @covers Revision::loadFromPageId
912 */
913 public function testLoadFromPageIdWithNotLatestRevId() {
914 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
915 $this->assertRevEquals(
916 $this->testPage->getRevision()->getPrevious(),
917 Revision::loadFromPageId(
918 wfGetDB( DB_MASTER ),
919 $this->testPage->getId(),
920 $this->testPage->getRevision()->getPrevious()->getId()
921 )
922 );
923 }
924
925 /**
926 * @covers Revision::loadFromTitle
927 */
928 public function testLoadFromTitle() {
929 $this->assertRevEquals(
930 $this->testPage->getRevision(),
931 Revision::loadFromTitle( wfGetDB( DB_MASTER ), $this->testPage->getTitle() )
932 );
933 }
934
935 /**
936 * @covers Revision::loadFromTitle
937 */
938 public function testLoadFromTitleWithLatestRevId() {
939 $this->assertRevEquals(
940 $this->testPage->getRevision(),
941 Revision::loadFromTitle(
942 wfGetDB( DB_MASTER ),
943 $this->testPage->getTitle(),
944 $this->testPage->getLatest()
945 )
946 );
947 }
948
949 /**
950 * @covers Revision::loadFromTitle
951 */
952 public function testLoadFromTitleWithNotLatestRevId() {
953 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
954 $this->assertRevEquals(
955 $this->testPage->getRevision()->getPrevious(),
956 Revision::loadFromTitle(
957 wfGetDB( DB_MASTER ),
958 $this->testPage->getTitle(),
959 $this->testPage->getRevision()->getPrevious()->getId()
960 )
961 );
962 }
963
964 /**
965 * @covers Revision::loadFromTimestamp()
966 */
967 public function testLoadFromTimestamp() {
968 $this->assertRevEquals(
969 $this->testPage->getRevision(),
970 Revision::loadFromTimestamp(
971 wfGetDB( DB_MASTER ),
972 $this->testPage->getTitle(),
973 $this->testPage->getRevision()->getTimestamp()
974 )
975 );
976 }
977
978 /**
979 * @covers Revision::getParentLengths
980 */
981 public function testGetParentLengths_noRevIds() {
982 $this->assertSame(
983 [],
984 Revision::getParentLengths(
985 wfGetDB( DB_MASTER ),
986 []
987 )
988 );
989 }
990
991 /**
992 * @covers Revision::getParentLengths
993 */
994 public function testGetParentLengths_oneRevId() {
995 $text = '831jr091jr0921kr21kr0921kjr0921j09rj1';
996 $textLength = strlen( $text );
997
998 $this->testPage->doEditContent( new WikitextContent( $text ), __METHOD__ );
999 $rev[1] = $this->testPage->getLatest();
1000
1001 $this->assertSame(
1002 [ $rev[1] => strval( $textLength ) ],
1003 Revision::getParentLengths(
1004 wfGetDB( DB_MASTER ),
1005 [ $rev[1] ]
1006 )
1007 );
1008 }
1009
1010 /**
1011 * @covers Revision::getParentLengths
1012 */
1013 public function testGetParentLengths_multipleRevIds() {
1014 $textOne = '831jr091jr0921kr21kr0921kjr0921j09rj1';
1015 $textOneLength = strlen( $textOne );
1016 $textTwo = '831jr091jr092121j09rj1';
1017 $textTwoLength = strlen( $textTwo );
1018
1019 $this->testPage->doEditContent( new WikitextContent( $textOne ), __METHOD__ );
1020 $rev[1] = $this->testPage->getLatest();
1021 $this->testPage->doEditContent( new WikitextContent( $textTwo ), __METHOD__ );
1022 $rev[2] = $this->testPage->getLatest();
1023
1024 $this->assertSame(
1025 [ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
1026 Revision::getParentLengths(
1027 wfGetDB( DB_MASTER ),
1028 [ $rev[1], $rev[2] ]
1029 )
1030 );
1031 }
1032
1033 /**
1034 * @covers Revision::getTitle
1035 */
1036 public function testGetTitle_fromExistingRevision() {
1037 $this->assertTrue(
1038 $this->testPage->getTitle()->equals(
1039 $this->testPage->getRevision()->getTitle()
1040 )
1041 );
1042 }
1043
1044 /**
1045 * @covers Revision::getTitle
1046 */
1047 public function testGetTitle_fromRevisionWhichWillLoadTheTitle() {
1048 $rev = new Revision( [ 'id' => $this->testPage->getLatest() ] );
1049 $this->assertTrue(
1050 $this->testPage->getTitle()->equals(
1051 $rev->getTitle()
1052 )
1053 );
1054 }
1055
1056 /**
1057 * @covers Revision::getTitle
1058 */
1059 public function testGetTitle_forBadRevision() {
1060 $rev = new Revision( [] );
1061 $this->assertNull( $rev->getTitle() );
1062 }
1063
1064 /**
1065 * @covers Revision::isMinor
1066 */
1067 public function testIsMinor_true() {
1068 // Use a sysop to ensure we can mark edits as minor
1069 $sysop = $this->getTestSysop()->getUser();
1070
1071 $this->testPage->doEditContent(
1072 new WikitextContent( __METHOD__ ),
1073 __METHOD__,
1074 EDIT_MINOR,
1075 false,
1076 $sysop
1077 );
1078 $rev = $this->testPage->getRevision();
1079
1080 $this->assertSame( true, $rev->isMinor() );
1081 }
1082
1083 /**
1084 * @covers Revision::isMinor
1085 */
1086 public function testIsMinor_false() {
1087 $this->testPage->doEditContent(
1088 new WikitextContent( __METHOD__ ),
1089 __METHOD__,
1090 0
1091 );
1092 $rev = $this->testPage->getRevision();
1093
1094 $this->assertSame( false, $rev->isMinor() );
1095 }
1096
1097 /**
1098 * @covers Revision::getTimestamp
1099 */
1100 public function testGetTimestamp() {
1101 $testTimestamp = wfTimestampNow();
1102
1103 $this->testPage->doEditContent(
1104 new WikitextContent( __METHOD__ ),
1105 __METHOD__
1106 );
1107 $rev = $this->testPage->getRevision();
1108
1109 $this->assertInternalType( 'string', $rev->getTimestamp() );
1110 $this->assertTrue( strlen( $rev->getTimestamp() ) == strlen( 'YYYYMMDDHHMMSS' ) );
1111 $this->assertContains( substr( $testTimestamp, 0, 10 ), $rev->getTimestamp() );
1112 }
1113
1114 /**
1115 * @covers Revision::getUser
1116 * @covers Revision::getUserText
1117 */
1118 public function testGetUserAndText() {
1119 $sysop = $this->getTestSysop()->getUser();
1120
1121 $this->testPage->doEditContent(
1122 new WikitextContent( __METHOD__ ),
1123 __METHOD__,
1124 0,
1125 false,
1126 $sysop
1127 );
1128 $rev = $this->testPage->getRevision();
1129
1130 $this->assertSame( $sysop->getId(), $rev->getUser() );
1131 $this->assertSame( $sysop->getName(), $rev->getUserText() );
1132 }
1133
1134 /**
1135 * @covers Revision::isDeleted
1136 */
1137 public function testIsDeleted_nothingDeleted() {
1138 $rev = $this->testPage->getRevision();
1139
1140 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
1141 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_COMMENT ) );
1142 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
1143 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_USER ) );
1144 }
1145
1146 /**
1147 * @covers Revision::getVisibility
1148 */
1149 public function testGetVisibility_nothingDeleted() {
1150 $rev = $this->testPage->getRevision();
1151
1152 $this->assertSame( 0, $rev->getVisibility() );
1153 }
1154
1155 /**
1156 * @covers Revision::getComment
1157 */
1158 public function testGetComment_notDeleted() {
1159 $expectedSummary = 'goatlicious summary';
1160
1161 $this->testPage->doEditContent(
1162 new WikitextContent( __METHOD__ ),
1163 $expectedSummary
1164 );
1165 $rev = $this->testPage->getRevision();
1166
1167 $this->assertSame( $expectedSummary, $rev->getComment() );
1168 }
1169
1170 /**
1171 * @covers Revision::isUnpatrolled
1172 */
1173 public function testIsUnpatrolled_returnsRecentChangesId() {
1174 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1175 $rev = $this->testPage->getRevision();
1176
1177 $this->assertGreaterThan( 0, $rev->isUnpatrolled() );
1178 $this->assertSame( $rev->getRecentChange()->getAttribute( 'rc_id' ), $rev->isUnpatrolled() );
1179 }
1180
1181 /**
1182 * @covers Revision::isUnpatrolled
1183 */
1184 public function testIsUnpatrolled_returnsZeroIfPatrolled() {
1185 // This assumes that sysops are auto patrolled
1186 $sysop = $this->getTestSysop()->getUser();
1187 $this->testPage->doEditContent(
1188 new WikitextContent( __METHOD__ ),
1189 __METHOD__,
1190 0,
1191 false,
1192 $sysop
1193 );
1194 $rev = $this->testPage->getRevision();
1195
1196 $this->assertSame( 0, $rev->isUnpatrolled() );
1197 }
1198
1199 /**
1200 * This is a simple blanket test for all simple content getters and is methods to provide some
1201 * coverage before the split of Revision into multiple classes for MCR work.
1202 * @covers Revision::getContent
1203 * @covers Revision::getSerializedData
1204 * @covers Revision::getContentModel
1205 * @covers Revision::getContentFormat
1206 * @covers Revision::getContentHandler
1207 */
1208 public function testSimpleContentGetters() {
1209 $expectedText = 'testSimpleContentGetters in Revision. Goats love MCR...';
1210 $expectedSummary = 'goatlicious testSimpleContentGetters summary';
1211
1212 $this->testPage->doEditContent(
1213 new WikitextContent( $expectedText ),
1214 $expectedSummary
1215 );
1216 $rev = $this->testPage->getRevision();
1217
1218 $this->assertSame( $expectedText, $rev->getContent()->getNativeData() );
1219 $this->assertSame( $expectedText, $rev->getSerializedData() );
1220 $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() );
1221 $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() );
1222 $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() );
1223 }
1224
1225 /**
1226 * @covers Revision::newKnownCurrent
1227 */
1228 public function testNewKnownCurrent() {
1229 // Setup the services
1230 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
1231 $this->setService( 'MainWANObjectCache', $cache );
1232 $db = wfGetDB( DB_MASTER );
1233
1234 // Get a fresh revision to use during testing
1235 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1236 $rev = $this->testPage->getRevision();
1237
1238 // Clear any previous cache for the revision during creation
1239 $key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
1240 $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
1241 $this->assertFalse( $cache->get( $key ) );
1242
1243 // Get the new revision and make sure it is in the cache and correct
1244 $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
1245 $this->assertRevEquals( $rev, $newRev );
1246 $this->assertRevEquals( $rev, $cache->get( $key ) );
1247 }
1248
1249 public function provideUserCanBitfield() {
1250 yield [ 0, 0, [], null, true ];
1251 // Bitfields match, user has no permissions
1252 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], null, false ];
1253 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], null, false ];
1254 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], null, false ];
1255 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], null, false ];
1256 // Bitfields match, user (admin) does have permissions
1257 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], null, true ];
1258 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], null, true ];
1259 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], null, true ];
1260 // Bitfields match, user (admin) does not have permissions
1261 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], null, false ];
1262 // Bitfields match, user (oversight) does have permissions
1263 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], null, true ];
1264 // Check permissions using the title
1265 yield [
1266 Revision::DELETED_TEXT,
1267 Revision::DELETED_TEXT,
1268 [ 'sysop' ],
1269 Title::newFromText( __METHOD__ ),
1270 true,
1271 ];
1272 yield [
1273 Revision::DELETED_TEXT,
1274 Revision::DELETED_TEXT,
1275 [],
1276 Title::newFromText( __METHOD__ ),
1277 false,
1278 ];
1279 }
1280
1281 /**
1282 * @dataProvider provideUserCanBitfield
1283 * @covers Revision::userCanBitfield
1284 */
1285 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
1286 $this->setMwGlobals(
1287 'wgGroupPermissions',
1288 [
1289 'sysop' => [
1290 'deletedtext' => true,
1291 'deletedhistory' => true,
1292 ],
1293 'oversight' => [
1294 'viewsuppressed' => true,
1295 'suppressrevision' => true,
1296 ],
1297 ]
1298 );
1299 $user = $this->getTestUser( $userGroups )->getUser();
1300
1301 $this->assertSame(
1302 $expected,
1303 Revision::userCanBitfield( $bitField, $field, $user, $title )
1304 );
1305 }
1306
1307 public function provideUserCan() {
1308 yield [ 0, 0, [], true ];
1309 // Bitfields match, user has no permissions
1310 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], false ];
1311 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], false ];
1312 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], false ];
1313 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], false ];
1314 // Bitfields match, user (admin) does have permissions
1315 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], true ];
1316 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], true ];
1317 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], true ];
1318 // Bitfields match, user (admin) does not have permissions
1319 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], false ];
1320 // Bitfields match, user (oversight) does have permissions
1321 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], true ];
1322 }
1323
1324 /**
1325 * @dataProvider provideUserCan
1326 * @covers Revision::userCan
1327 */
1328 public function testUserCan( $bitField, $field, $userGroups, $expected ) {
1329 $this->setMwGlobals(
1330 'wgGroupPermissions',
1331 [
1332 'sysop' => [
1333 'deletedtext' => true,
1334 'deletedhistory' => true,
1335 ],
1336 'oversight' => [
1337 'viewsuppressed' => true,
1338 'suppressrevision' => true,
1339 ],
1340 ]
1341 );
1342 $user = $this->getTestUser( $userGroups )->getUser();
1343 $revision = new Revision( [ 'deleted' => $bitField ] );
1344
1345 $this->assertSame(
1346 $expected,
1347 $revision->userCan( $field, $user )
1348 );
1349 }
1350
1351 }