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