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