RevisionStorageTest: Add tests for Revision::newFromTitle()
[lhc/web/wiklou.git] / tests / phpunit / includes / RevisionStorageTest.php
1 <?php
2
3 /**
4 * Test class for Revision storage.
5 *
6 * @group ContentHandler
7 * @group Database
8 * ^--- important, causes temporary tables to be used instead of the real database
9 *
10 * @group medium
11 * ^--- important, causes tests not to fail with timeout
12 */
13 class RevisionStorageTest extends MediaWikiTestCase {
14 /**
15 * @var WikiPage $the_page
16 */
17 private $the_page;
18
19 public function __construct( $name = null, array $data = [], $dataName = '' ) {
20 parent::__construct( $name, $data, $dataName );
21
22 $this->tablesUsed = array_merge( $this->tablesUsed,
23 [ 'page',
24 'revision',
25 'ip_changes',
26 'text',
27
28 'recentchanges',
29 'logging',
30
31 'page_props',
32 'pagelinks',
33 'categorylinks',
34 'langlinks',
35 'externallinks',
36 'imagelinks',
37 'templatelinks',
38 'iwlinks' ] );
39 }
40
41 protected function setUp() {
42 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
43
44 parent::setUp();
45
46 $wgExtraNamespaces[12312] = 'Dummy';
47 $wgExtraNamespaces[12313] = 'Dummy_talk';
48
49 $wgNamespaceContentModels[12312] = 'DUMMY';
50 $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting';
51
52 MWNamespace::clearCaches();
53 // Reset namespace cache
54 $wgContLang->resetNamespaces();
55 if ( !$this->the_page ) {
56 $this->the_page = $this->createPage(
57 'RevisionStorageTest_the_page',
58 "just a dummy page",
59 CONTENT_MODEL_WIKITEXT
60 );
61 }
62
63 $this->tablesUsed[] = 'archive';
64 }
65
66 protected function tearDown() {
67 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
68
69 parent::tearDown();
70
71 unset( $wgExtraNamespaces[12312] );
72 unset( $wgExtraNamespaces[12313] );
73
74 unset( $wgNamespaceContentModels[12312] );
75 unset( $wgContentHandlers['DUMMY'] );
76
77 MWNamespace::clearCaches();
78 // Reset namespace cache
79 $wgContLang->resetNamespaces();
80 }
81
82 protected function makeRevision( $props = null ) {
83 if ( $props === null ) {
84 $props = [];
85 }
86
87 if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
88 $props['text'] = 'Lorem Ipsum';
89 }
90
91 if ( !isset( $props['comment'] ) ) {
92 $props['comment'] = 'just a test';
93 }
94
95 if ( !isset( $props['page'] ) ) {
96 $props['page'] = $this->the_page->getId();
97 }
98
99 $rev = new Revision( $props );
100
101 $dbw = wfGetDB( DB_MASTER );
102 $rev->insertOn( $dbw );
103
104 return $rev;
105 }
106
107 /**
108 * @param string $titleString
109 * @param string $text
110 * @param string|null $model
111 *
112 * @return WikiPage
113 */
114 protected function createPage( $titleString, $text, $model = null ) {
115 if ( !preg_match( '/:/', $titleString ) &&
116 ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
117 ) {
118 $ns = $this->getDefaultWikitextNS();
119 $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
120 }
121
122 $title = Title::newFromText( $titleString );
123 $wikipage = new WikiPage( $title );
124
125 // Delete the article if it already exists
126 if ( $wikipage->exists() ) {
127 $wikipage->doDeleteArticle( "done" );
128 }
129
130 $content = ContentHandler::makeContent( $text, $title, $model );
131 $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW );
132
133 return $wikipage;
134 }
135
136 protected function assertRevEquals( Revision $orig, Revision $rev = null ) {
137 $this->assertNotNull( $rev, 'missing revision' );
138
139 $this->assertEquals( $orig->getId(), $rev->getId() );
140 $this->assertEquals( $orig->getPage(), $rev->getPage() );
141 $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
142 $this->assertEquals( $orig->getUser(), $rev->getUser() );
143 $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
144 $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
145 $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
146 }
147
148 /**
149 * @covers Revision::__construct
150 */
151 public function testConstructFromRow() {
152 $orig = $this->makeRevision();
153
154 $dbr = wfGetDB( DB_REPLICA );
155 $res = $dbr->select( 'revision', Revision::selectFields(), [ 'rev_id' => $orig->getId() ] );
156 $this->assertTrue( is_object( $res ), 'query failed' );
157
158 $row = $res->fetchObject();
159 $res->free();
160
161 $rev = new Revision( $row );
162
163 $this->assertRevEquals( $orig, $rev );
164 }
165
166 /**
167 * @covers Revision::newFromTitle
168 */
169 public function testNewFromTitle_withoutId() {
170 $page = $this->createPage(
171 __METHOD__,
172 'GOAT',
173 CONTENT_MODEL_WIKITEXT
174 );
175 $latestRevId = $page->getLatest();
176
177 $rev = Revision::newFromTitle( $page->getTitle() );
178
179 $this->assertTrue( $page->getTitle()->equals( $rev->getTitle() ) );
180 $this->assertEquals( $latestRevId, $rev->getId() );
181 }
182
183 /**
184 * @covers Revision::newFromTitle
185 */
186 public function testNewFromTitle_withId() {
187 $page = $this->createPage(
188 __METHOD__,
189 'GOAT',
190 CONTENT_MODEL_WIKITEXT
191 );
192 $latestRevId = $page->getLatest();
193
194 $rev = Revision::newFromTitle( $page->getTitle(), $latestRevId );
195
196 $this->assertTrue( $page->getTitle()->equals( $rev->getTitle() ) );
197 $this->assertEquals( $latestRevId, $rev->getId() );
198 }
199
200 /**
201 * @covers Revision::newFromTitle
202 */
203 public function testNewFromTitle_withBadId() {
204 $page = $this->createPage(
205 __METHOD__,
206 'GOAT',
207 CONTENT_MODEL_WIKITEXT
208 );
209 $latestRevId = $page->getLatest();
210
211 $rev = Revision::newFromTitle( $page->getTitle(), $latestRevId + 1 );
212
213 $this->assertNull( $rev );
214 }
215
216 /**
217 * @covers Revision::newFromRow
218 */
219 public function testNewFromRow() {
220 $orig = $this->makeRevision();
221
222 $dbr = wfGetDB( DB_REPLICA );
223 $res = $dbr->select( 'revision', Revision::selectFields(), [ 'rev_id' => $orig->getId() ] );
224 $this->assertTrue( is_object( $res ), 'query failed' );
225
226 $row = $res->fetchObject();
227 $res->free();
228
229 $rev = Revision::newFromRow( $row );
230
231 $this->assertRevEquals( $orig, $rev );
232 }
233
234 /**
235 * @covers Revision::newFromArchiveRow
236 */
237 public function testNewFromArchiveRow() {
238 $page = $this->createPage(
239 'RevisionStorageTest_testNewFromArchiveRow',
240 'Lorem Ipsum',
241 CONTENT_MODEL_WIKITEXT
242 );
243 $orig = $page->getRevision();
244 $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
245
246 $dbr = wfGetDB( DB_REPLICA );
247 $res = $dbr->select(
248 'archive', Revision::selectArchiveFields(), [ 'ar_rev_id' => $orig->getId() ]
249 );
250 $this->assertTrue( is_object( $res ), 'query failed' );
251
252 $row = $res->fetchObject();
253 $res->free();
254
255 $rev = Revision::newFromArchiveRow( $row );
256
257 $this->assertRevEquals( $orig, $rev );
258 }
259
260 /**
261 * @covers Revision::newFromId
262 */
263 public function testNewFromId() {
264 $orig = $this->makeRevision();
265
266 $rev = Revision::newFromId( $orig->getId() );
267
268 $this->assertRevEquals( $orig, $rev );
269 }
270
271 /**
272 * @covers Revision::fetchRevision
273 */
274 public function testFetchRevision() {
275 $page = $this->createPage(
276 'RevisionStorageTest_testFetchRevision',
277 'one',
278 CONTENT_MODEL_WIKITEXT
279 );
280
281 // Hidden process cache assertion below
282 $page->getRevision()->getId();
283
284 $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
285 $id = $page->getRevision()->getId();
286
287 $res = Revision::fetchRevision( $page->getTitle() );
288
289 # note: order is unspecified
290 $rows = [];
291 while ( ( $row = $res->fetchObject() ) ) {
292 $rows[$row->rev_id] = $row;
293 }
294
295 $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
296 $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
297 }
298
299 /**
300 * @covers Revision::selectFields
301 */
302 public function testSelectFields() {
303 global $wgContentHandlerUseDB;
304
305 $fields = Revision::selectFields();
306
307 $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields' );
308 $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields' );
309 $this->assertTrue(
310 in_array( 'rev_timestamp', $fields ),
311 'missing rev_timestamp in list of fields'
312 );
313 $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields' );
314
315 if ( $wgContentHandlerUseDB ) {
316 $this->assertTrue( in_array( 'rev_content_model', $fields ),
317 'missing rev_content_model in list of fields' );
318 $this->assertTrue( in_array( 'rev_content_format', $fields ),
319 'missing rev_content_format in list of fields' );
320 }
321 }
322
323 /**
324 * @covers Revision::getPage
325 */
326 public function testGetPage() {
327 $page = $this->the_page;
328
329 $orig = $this->makeRevision( [ 'page' => $page->getId() ] );
330 $rev = Revision::newFromId( $orig->getId() );
331
332 $this->assertEquals( $page->getId(), $rev->getPage() );
333 }
334
335 /**
336 * @covers Revision::getContent
337 */
338 public function testGetContent_failure() {
339 $rev = new Revision( [
340 'page' => $this->the_page->getId(),
341 'content_model' => $this->the_page->getContentModel(),
342 'text_id' => 123456789, // not in the test DB
343 ] );
344
345 $this->assertNull( $rev->getContent(),
346 "getContent() should return null if the revision's text blob could not be loaded." );
347
348 // NOTE: check this twice, once for lazy initialization, and once with the cached value.
349 $this->assertNull( $rev->getContent(),
350 "getContent() should return null if the revision's text blob could not be loaded." );
351 }
352
353 /**
354 * @covers Revision::getContent
355 */
356 public function testGetContent() {
357 $orig = $this->makeRevision( [ 'text' => 'hello hello.' ] );
358 $rev = Revision::newFromId( $orig->getId() );
359
360 $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
361 }
362
363 /**
364 * @covers Revision::getContentModel
365 */
366 public function testGetContentModel() {
367 global $wgContentHandlerUseDB;
368
369 if ( !$wgContentHandlerUseDB ) {
370 $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
371 }
372
373 $orig = $this->makeRevision( [ 'text' => 'hello hello.',
374 'content_model' => CONTENT_MODEL_JAVASCRIPT ] );
375 $rev = Revision::newFromId( $orig->getId() );
376
377 $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
378 }
379
380 /**
381 * @covers Revision::getContentFormat
382 */
383 public function testGetContentFormat() {
384 global $wgContentHandlerUseDB;
385
386 if ( !$wgContentHandlerUseDB ) {
387 $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
388 }
389
390 $orig = $this->makeRevision( [
391 'text' => 'hello hello.',
392 'content_model' => CONTENT_MODEL_JAVASCRIPT,
393 'content_format' => CONTENT_FORMAT_JAVASCRIPT
394 ] );
395 $rev = Revision::newFromId( $orig->getId() );
396
397 $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
398 }
399
400 /**
401 * @covers Revision::isCurrent
402 */
403 public function testIsCurrent() {
404 $page = $this->createPage(
405 'RevisionStorageTest_testIsCurrent',
406 'Lorem Ipsum',
407 CONTENT_MODEL_WIKITEXT
408 );
409 $rev1 = $page->getRevision();
410
411 # @todo find out if this should be true
412 # $this->assertTrue( $rev1->isCurrent() );
413
414 $rev1x = Revision::newFromId( $rev1->getId() );
415 $this->assertTrue( $rev1x->isCurrent() );
416
417 $page->doEditContent(
418 ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
419 'second rev'
420 );
421 $rev2 = $page->getRevision();
422
423 # @todo find out if this should be true
424 # $this->assertTrue( $rev2->isCurrent() );
425
426 $rev1x = Revision::newFromId( $rev1->getId() );
427 $this->assertFalse( $rev1x->isCurrent() );
428
429 $rev2x = Revision::newFromId( $rev2->getId() );
430 $this->assertTrue( $rev2x->isCurrent() );
431 }
432
433 /**
434 * @covers Revision::getPrevious
435 */
436 public function testGetPrevious() {
437 $page = $this->createPage(
438 'RevisionStorageTest_testGetPrevious',
439 'Lorem Ipsum testGetPrevious',
440 CONTENT_MODEL_WIKITEXT
441 );
442 $rev1 = $page->getRevision();
443
444 $this->assertNull( $rev1->getPrevious() );
445
446 $page->doEditContent(
447 ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
448 'second rev testGetPrevious' );
449 $rev2 = $page->getRevision();
450
451 $this->assertNotNull( $rev2->getPrevious() );
452 $this->assertEquals( $rev1->getId(), $rev2->getPrevious()->getId() );
453 }
454
455 /**
456 * @covers Revision::getNext
457 */
458 public function testGetNext() {
459 $page = $this->createPage(
460 'RevisionStorageTest_testGetNext',
461 'Lorem Ipsum testGetNext',
462 CONTENT_MODEL_WIKITEXT
463 );
464 $rev1 = $page->getRevision();
465
466 $this->assertNull( $rev1->getNext() );
467
468 $page->doEditContent(
469 ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
470 'second rev testGetNext'
471 );
472 $rev2 = $page->getRevision();
473
474 $this->assertNotNull( $rev1->getNext() );
475 $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
476 }
477
478 /**
479 * @covers Revision::newNullRevision
480 */
481 public function testNewNullRevision() {
482 $page = $this->createPage(
483 'RevisionStorageTest_testNewNullRevision',
484 'some testing text',
485 CONTENT_MODEL_WIKITEXT
486 );
487 $orig = $page->getRevision();
488
489 $dbw = wfGetDB( DB_MASTER );
490 $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
491
492 $this->assertNotEquals( $orig->getId(), $rev->getId(),
493 'new null revision shold have a different id from the original revision' );
494 $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
495 'new null revision shold have the same text id as the original revision' );
496 $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
497 }
498
499 /**
500 * @covers Revision::insertOn
501 */
502 public function testInsertOn() {
503 $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
504
505 $orig = $this->makeRevision( [
506 'user_text' => $ip
507 ] );
508
509 // Make sure the revision was copied to ip_changes
510 $dbr = wfGetDB( DB_REPLICA );
511 $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
512 $row = $res->fetchObject();
513
514 $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
515 $this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp );
516 }
517
518 public static function provideUserWasLastToEdit() {
519 yield 'actually the last edit' => [ 3, true ];
520 yield 'not the current edit, but still by this user' => [ 2, true ];
521 yield 'edit by another user' => [ 1, false ];
522 yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ];
523 }
524
525 /**
526 * @dataProvider provideUserWasLastToEdit
527 */
528 public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
529 $userA = User::newFromName( "RevisionStorageTest_userA" );
530 $userB = User::newFromName( "RevisionStorageTest_userB" );
531
532 if ( $userA->getId() === 0 ) {
533 $userA = User::createNew( $userA->getName() );
534 }
535
536 if ( $userB->getId() === 0 ) {
537 $userB = User::createNew( $userB->getName() );
538 }
539
540 $ns = $this->getDefaultWikitextNS();
541
542 $dbw = wfGetDB( DB_MASTER );
543 $revisions = [];
544
545 // create revisions -----------------------------
546 $page = WikiPage::factory( Title::newFromText(
547 'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
548 $page->insertOn( $dbw );
549
550 $revisions[0] = new Revision( [
551 'page' => $page->getId(),
552 // we need the title to determine the page's default content model
553 'title' => $page->getTitle(),
554 'timestamp' => '20120101000000',
555 'user' => $userA->getId(),
556 'text' => 'zero',
557 'content_model' => CONTENT_MODEL_WIKITEXT,
558 'summary' => 'edit zero'
559 ] );
560 $revisions[0]->insertOn( $dbw );
561
562 $revisions[1] = new Revision( [
563 'page' => $page->getId(),
564 // still need the title, because $page->getId() is 0 (there's no entry in the page table)
565 'title' => $page->getTitle(),
566 'timestamp' => '20120101000100',
567 'user' => $userA->getId(),
568 'text' => 'one',
569 'content_model' => CONTENT_MODEL_WIKITEXT,
570 'summary' => 'edit one'
571 ] );
572 $revisions[1]->insertOn( $dbw );
573
574 $revisions[2] = new Revision( [
575 'page' => $page->getId(),
576 'title' => $page->getTitle(),
577 'timestamp' => '20120101000200',
578 'user' => $userB->getId(),
579 'text' => 'two',
580 'content_model' => CONTENT_MODEL_WIKITEXT,
581 'summary' => 'edit two'
582 ] );
583 $revisions[2]->insertOn( $dbw );
584
585 $revisions[3] = new Revision( [
586 'page' => $page->getId(),
587 'title' => $page->getTitle(),
588 'timestamp' => '20120101000300',
589 'user' => $userA->getId(),
590 'text' => 'three',
591 'content_model' => CONTENT_MODEL_WIKITEXT,
592 'summary' => 'edit three'
593 ] );
594 $revisions[3]->insertOn( $dbw );
595
596 $revisions[4] = new Revision( [
597 'page' => $page->getId(),
598 'title' => $page->getTitle(),
599 'timestamp' => '20120101000200',
600 'user' => $userA->getId(),
601 'text' => 'zero',
602 'content_model' => CONTENT_MODEL_WIKITEXT,
603 'summary' => 'edit four'
604 ] );
605 $revisions[4]->insertOn( $dbw );
606
607 // test it ---------------------------------
608 $since = $revisions[$sinceIdx]->getTimestamp();
609
610 $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
611
612 $this->assertEquals( $expectedLast, $wasLast );
613 }
614 }