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