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