Merge "debug: Allow the DBQuery channel to be used"
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStore;
6 use HashBagOStuff;
7 use InvalidArgumentException;
8 use Language;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Revision\RevisionAccessException;
11 use MediaWiki\Revision\RevisionStore;
12 use MediaWiki\Revision\SlotRecord;
13 use MediaWiki\Storage\SqlBlobStore;
14 use MediaWikiTestCase;
15 use MWException;
16 use Title;
17 use WANObjectCache;
18 use Wikimedia\Rdbms\Database;
19 use Wikimedia\Rdbms\LoadBalancer;
20 use Wikimedia\TestingAccessWrapper;
21 use WikitextContent;
22
23 class RevisionStoreTest extends MediaWikiTestCase {
24
25 private function useTextId() {
26 global $wgMultiContentRevisionSchemaMigrationStage;
27
28 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
29 }
30
31 /**
32 * @param LoadBalancer $loadBalancer
33 * @param SqlBlobStore $blobStore
34 * @param WANObjectCache $WANObjectCache
35 *
36 * @return RevisionStore
37 */
38 private function getRevisionStore(
39 $loadBalancer = null,
40 $blobStore = null,
41 $WANObjectCache = null
42 ) {
43 global $wgMultiContentRevisionSchemaMigrationStage;
44 // the migration stage should be irrelevant, since all the tests that interact with
45 // the database are in RevisionStoreDbTest, not here.
46
47 return new RevisionStore(
48 $loadBalancer ?: $this->getMockLoadBalancer(),
49 $blobStore ?: $this->getMockSqlBlobStore(),
50 $WANObjectCache ?: $this->getHashWANObjectCache(),
51 MediaWikiServices::getInstance()->getCommentStore(),
52 MediaWikiServices::getInstance()->getContentModelStore(),
53 MediaWikiServices::getInstance()->getSlotRoleStore(),
54 $wgMultiContentRevisionSchemaMigrationStage,
55 MediaWikiServices::getInstance()->getActorMigration()
56 );
57 }
58
59 /**
60 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
61 */
62 private function getMockLoadBalancer() {
63 return $this->getMockBuilder( LoadBalancer::class )
64 ->disableOriginalConstructor()->getMock();
65 }
66
67 /**
68 * @return \PHPUnit_Framework_MockObject_MockObject|Database
69 */
70 private function getMockDatabase() {
71 return $this->getMockBuilder( Database::class )
72 ->disableOriginalConstructor()->getMock();
73 }
74
75 /**
76 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
77 */
78 private function getMockSqlBlobStore() {
79 return $this->getMockBuilder( SqlBlobStore::class )
80 ->disableOriginalConstructor()->getMock();
81 }
82
83 /**
84 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
85 */
86 private function getMockCommentStore() {
87 return $this->getMockBuilder( CommentStore::class )
88 ->disableOriginalConstructor()->getMock();
89 }
90
91 private function getHashWANObjectCache() {
92 return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
93 }
94
95 public function provideSetContentHandlerUseDB() {
96 return [
97 // ContentHandlerUseDB can be true of false pre migration.
98 [ false, SCHEMA_COMPAT_OLD, false ],
99 [ true, SCHEMA_COMPAT_OLD, false ],
100 // During and after migration it can not be false...
101 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
102 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
103 [ false, SCHEMA_COMPAT_NEW, true ],
104 // ...but it can be true.
105 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
106 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
107 [ true, SCHEMA_COMPAT_NEW, false ],
108 ];
109 }
110
111 /**
112 * @dataProvider provideSetContentHandlerUseDB
113 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
114 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
115 */
116 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
117 if ( $expectedFail ) {
118 $this->setExpectedException( MWException::class );
119 }
120
121 $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
122
123 $store = new RevisionStore(
124 $this->getMockLoadBalancer(),
125 $this->getMockSqlBlobStore(),
126 $this->getHashWANObjectCache(),
127 $this->getMockCommentStore(),
128 $nameTables->getContentModels(),
129 $nameTables->getSlotRoles(),
130 $migrationMode,
131 MediaWikiServices::getInstance()->getActorMigration()
132 );
133
134 $store->setContentHandlerUseDB( $contentHandlerDb );
135 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
136 }
137
138 public function testGetTitle_successFromPageId() {
139 $mockLoadBalancer = $this->getMockLoadBalancer();
140 // Title calls wfGetDB() so we have to set the main service
141 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
142
143 $db = $this->getMockDatabase();
144 // Title calls wfGetDB() which uses a regular Connection
145 $mockLoadBalancer->expects( $this->atLeastOnce() )
146 ->method( 'getConnection' )
147 ->willReturn( $db );
148
149 // First call to Title::newFromID, faking no result (db lag?)
150 $db->expects( $this->at( 0 ) )
151 ->method( 'selectRow' )
152 ->with(
153 'page',
154 $this->anything(),
155 [ 'page_id' => 1 ]
156 )
157 ->willReturn( (object)[
158 'page_namespace' => '1',
159 'page_title' => 'Food',
160 ] );
161
162 $store = $this->getRevisionStore( $mockLoadBalancer );
163 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
164
165 $this->assertSame( 1, $title->getNamespace() );
166 $this->assertSame( 'Food', $title->getDBkey() );
167 }
168
169 public function testGetTitle_successFromPageIdOnFallback() {
170 $mockLoadBalancer = $this->getMockLoadBalancer();
171 // Title calls wfGetDB() so we have to set the main service
172 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
173
174 $db = $this->getMockDatabase();
175 // Title calls wfGetDB() which uses a regular Connection
176 // Assert that the first call uses a REPLICA and the second falls back to master
177 $mockLoadBalancer->expects( $this->exactly( 2 ) )
178 ->method( 'getConnection' )
179 ->willReturn( $db );
180 // RevisionStore getTitle uses a ConnectionRef
181 $mockLoadBalancer->expects( $this->atLeastOnce() )
182 ->method( 'getConnectionRef' )
183 ->willReturn( $db );
184
185 // First call to Title::newFromID, faking no result (db lag?)
186 $db->expects( $this->at( 0 ) )
187 ->method( 'selectRow' )
188 ->with(
189 'page',
190 $this->anything(),
191 [ 'page_id' => 1 ]
192 )
193 ->willReturn( false );
194
195 // First select using rev_id, faking no result (db lag?)
196 $db->expects( $this->at( 1 ) )
197 ->method( 'selectRow' )
198 ->with(
199 [ 'revision', 'page' ],
200 $this->anything(),
201 [ 'rev_id' => 2 ]
202 )
203 ->willReturn( false );
204
205 // Second call to Title::newFromID, no result
206 $db->expects( $this->at( 2 ) )
207 ->method( 'selectRow' )
208 ->with(
209 'page',
210 $this->anything(),
211 [ 'page_id' => 1 ]
212 )
213 ->willReturn( (object)[
214 'page_namespace' => '2',
215 'page_title' => 'Foodey',
216 ] );
217
218 $store = $this->getRevisionStore( $mockLoadBalancer );
219 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
220
221 $this->assertSame( 2, $title->getNamespace() );
222 $this->assertSame( 'Foodey', $title->getDBkey() );
223 }
224
225 public function testGetTitle_successFromRevId() {
226 $mockLoadBalancer = $this->getMockLoadBalancer();
227 // Title calls wfGetDB() so we have to set the main service
228 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
229
230 $db = $this->getMockDatabase();
231 // Title calls wfGetDB() which uses a regular Connection
232 $mockLoadBalancer->expects( $this->atLeastOnce() )
233 ->method( 'getConnection' )
234 ->willReturn( $db );
235 // RevisionStore getTitle uses a ConnectionRef
236 $mockLoadBalancer->expects( $this->atLeastOnce() )
237 ->method( 'getConnectionRef' )
238 ->willReturn( $db );
239
240 // First call to Title::newFromID, faking no result (db lag?)
241 $db->expects( $this->at( 0 ) )
242 ->method( 'selectRow' )
243 ->with(
244 'page',
245 $this->anything(),
246 [ 'page_id' => 1 ]
247 )
248 ->willReturn( false );
249
250 // First select using rev_id, faking no result (db lag?)
251 $db->expects( $this->at( 1 ) )
252 ->method( 'selectRow' )
253 ->with(
254 [ 'revision', 'page' ],
255 $this->anything(),
256 [ 'rev_id' => 2 ]
257 )
258 ->willReturn( (object)[
259 'page_namespace' => '1',
260 'page_title' => 'Food2',
261 ] );
262
263 $store = $this->getRevisionStore( $mockLoadBalancer );
264 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
265
266 $this->assertSame( 1, $title->getNamespace() );
267 $this->assertSame( 'Food2', $title->getDBkey() );
268 }
269
270 public function testGetTitle_successFromRevIdOnFallback() {
271 $mockLoadBalancer = $this->getMockLoadBalancer();
272 // Title calls wfGetDB() so we have to set the main service
273 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
274
275 $db = $this->getMockDatabase();
276 // Title calls wfGetDB() which uses a regular Connection
277 // Assert that the first call uses a REPLICA and the second falls back to master
278 $mockLoadBalancer->expects( $this->exactly( 2 ) )
279 ->method( 'getConnection' )
280 ->willReturn( $db );
281 // RevisionStore getTitle uses a ConnectionRef
282 $mockLoadBalancer->expects( $this->atLeastOnce() )
283 ->method( 'getConnectionRef' )
284 ->willReturn( $db );
285
286 // First call to Title::newFromID, faking no result (db lag?)
287 $db->expects( $this->at( 0 ) )
288 ->method( 'selectRow' )
289 ->with(
290 'page',
291 $this->anything(),
292 [ 'page_id' => 1 ]
293 )
294 ->willReturn( false );
295
296 // First select using rev_id, faking no result (db lag?)
297 $db->expects( $this->at( 1 ) )
298 ->method( 'selectRow' )
299 ->with(
300 [ 'revision', 'page' ],
301 $this->anything(),
302 [ 'rev_id' => 2 ]
303 )
304 ->willReturn( false );
305
306 // Second call to Title::newFromID, no result
307 $db->expects( $this->at( 2 ) )
308 ->method( 'selectRow' )
309 ->with(
310 'page',
311 $this->anything(),
312 [ 'page_id' => 1 ]
313 )
314 ->willReturn( false );
315
316 // Second select using rev_id, result
317 $db->expects( $this->at( 3 ) )
318 ->method( 'selectRow' )
319 ->with(
320 [ 'revision', 'page' ],
321 $this->anything(),
322 [ 'rev_id' => 2 ]
323 )
324 ->willReturn( (object)[
325 'page_namespace' => '2',
326 'page_title' => 'Foodey',
327 ] );
328
329 $store = $this->getRevisionStore( $mockLoadBalancer );
330 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
331
332 $this->assertSame( 2, $title->getNamespace() );
333 $this->assertSame( 'Foodey', $title->getDBkey() );
334 }
335
336 /**
337 * @covers \MediaWiki\Revision\RevisionStore::getTitle
338 */
339 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
340 $mockLoadBalancer = $this->getMockLoadBalancer();
341 // Title calls wfGetDB() so we have to set the main service
342 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
343
344 $db = $this->getMockDatabase();
345 // Title calls wfGetDB() which uses a regular Connection
346 // Assert that the first call uses a REPLICA and the second falls back to master
347
348 // RevisionStore getTitle uses getConnectionRef
349 // Title::newFromID uses getConnection
350 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
351 $mockLoadBalancer->expects( $this->exactly( 2 ) )
352 ->method( $method )
353 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
354 static $callCounter = 0;
355 $callCounter++;
356 // The first call should be to a REPLICA, and the second a MASTER.
357 if ( $callCounter === 1 ) {
358 $this->assertSame( DB_REPLICA, $masterOrReplica );
359 } elseif ( $callCounter === 2 ) {
360 $this->assertSame( DB_MASTER, $masterOrReplica );
361 }
362 return $db;
363 } );
364 }
365 // First and third call to Title::newFromID, faking no result
366 foreach ( [ 0, 2 ] as $counter ) {
367 $db->expects( $this->at( $counter ) )
368 ->method( 'selectRow' )
369 ->with(
370 'page',
371 $this->anything(),
372 [ 'page_id' => 1 ]
373 )
374 ->willReturn( false );
375 }
376
377 foreach ( [ 1, 3 ] as $counter ) {
378 $db->expects( $this->at( $counter ) )
379 ->method( 'selectRow' )
380 ->with(
381 [ 'revision', 'page' ],
382 $this->anything(),
383 [ 'rev_id' => 2 ]
384 )
385 ->willReturn( false );
386 }
387
388 $store = $this->getRevisionStore( $mockLoadBalancer );
389
390 $this->setExpectedException( RevisionAccessException::class );
391 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
392 }
393
394 public function provideNewRevisionFromRow_legacyEncoding_applied() {
395 yield 'windows-1252, old_flags is empty' => [
396 'windows-1252',
397 'en',
398 [
399 'old_flags' => '',
400 'old_text' => "S\xF6me Content",
401 ],
402 'Söme Content'
403 ];
404
405 yield 'windows-1252, old_flags is null' => [
406 'windows-1252',
407 'en',
408 [
409 'old_flags' => null,
410 'old_text' => "S\xF6me Content",
411 ],
412 'Söme Content'
413 ];
414 }
415
416 /**
417 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
418 *
419 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
420 */
421 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
422 if ( !$this->useTextId() ) {
423 $this->markTestSkipped( 'No longer applicable with MCR schema' );
424 }
425
426 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
427 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
428
429 $blobStore = new SqlBlobStore( $lb, $cache );
430 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
431
432 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
433
434 $record = $store->newRevisionFromRow(
435 $this->makeRow( $row ),
436 0,
437 Title::newFromText( __METHOD__ . '-UTPage' )
438 );
439
440 $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
441 }
442
443 /**
444 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
445 */
446 public function testNewRevisionFromRow_legacyEncoding_ignored() {
447 if ( !$this->useTextId() ) {
448 $this->markTestSkipped( 'No longer applicable with MCR schema' );
449 }
450
451 $row = [
452 'old_flags' => 'utf-8',
453 'old_text' => 'Söme Content',
454 ];
455
456 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
457 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
458
459 $blobStore = new SqlBlobStore( $lb, $cache );
460 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
461
462 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
463
464 $record = $store->newRevisionFromRow(
465 $this->makeRow( $row ),
466 0,
467 Title::newFromText( __METHOD__ . '-UTPage' )
468 );
469 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
470 }
471
472 private function makeRow( array $array ) {
473 $row = $array + [
474 'rev_id' => 7,
475 'rev_page' => 5,
476 'rev_timestamp' => '20110101000000',
477 'rev_user_text' => 'Tester',
478 'rev_user' => 17,
479 'rev_minor_edit' => 0,
480 'rev_deleted' => 0,
481 'rev_len' => 100,
482 'rev_parent_id' => 0,
483 'rev_sha1' => 'deadbeef',
484 'rev_comment_text' => 'Testing',
485 'rev_comment_data' => '{}',
486 'rev_comment_cid' => 111,
487 'page_namespace' => 0,
488 'page_title' => 'TEST',
489 'page_id' => 5,
490 'page_latest' => 7,
491 'page_is_redirect' => 0,
492 'page_len' => 100,
493 'user_name' => 'Tester',
494 ];
495
496 if ( $this->useTextId() ) {
497 $row += [
498 'rev_content_format' => CONTENT_FORMAT_TEXT,
499 'rev_content_model' => CONTENT_MODEL_TEXT,
500 'rev_text_id' => 11,
501 'old_id' => 11,
502 'old_text' => 'Hello World',
503 'old_flags' => 'utf-8',
504 ];
505 } else {
506 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
507 $row['content'] = [
508 'main' => new WikitextContent( $array['old_text'] ),
509 ];
510 }
511 }
512
513 return (object)$row;
514 }
515
516 public function provideMigrationConstruction() {
517 return [
518 [ SCHEMA_COMPAT_OLD, false ],
519 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
520 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
521 [ SCHEMA_COMPAT_NEW, false ],
522 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
523 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
524 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
525 ];
526 }
527
528 /**
529 * @covers \MediaWiki\Revision\RevisionStore::__construct
530 * @dataProvider provideMigrationConstruction
531 */
532 public function testMigrationConstruction( $migration, $expectException ) {
533 if ( $expectException ) {
534 $this->setExpectedException( InvalidArgumentException::class );
535 }
536 $loadBalancer = $this->getMockLoadBalancer();
537 $blobStore = $this->getMockSqlBlobStore();
538 $cache = $this->getHashWANObjectCache();
539 $commentStore = $this->getMockCommentStore();
540 $services = MediaWikiServices::getInstance();
541 $nameTables = $services->getNameTableStoreFactory();
542 $contentModelStore = $nameTables->getContentModels();
543 $slotRoleStore = $nameTables->getSlotRoles();
544 $store = new RevisionStore(
545 $loadBalancer,
546 $blobStore,
547 $cache,
548 $commentStore,
549 $nameTables->getContentModels(),
550 $nameTables->getSlotRoles(),
551 $migration,
552 $services->getActorMigration()
553 );
554 if ( !$expectException ) {
555 $store = TestingAccessWrapper::newFromObject( $store );
556 $this->assertSame( $loadBalancer, $store->loadBalancer );
557 $this->assertSame( $blobStore, $store->blobStore );
558 $this->assertSame( $cache, $store->cache );
559 $this->assertSame( $commentStore, $store->commentStore );
560 $this->assertSame( $contentModelStore, $store->contentModelStore );
561 $this->assertSame( $slotRoleStore, $store->slotRoleStore );
562 $this->assertSame( $migration, $store->mcrMigrationStage );
563 }
564 }
565
566 }