Merge "selenium: invoke jobs to enforce eventual consistency"
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionRendererTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStoreComment;
6 use Content;
7 use Language;
8 use LogicException;
9 use MediaWiki\Revision\RevisionRenderer;
10 use MediaWiki\Storage\MutableRevisionRecord;
11 use MediaWiki\Storage\RevisionRecord;
12 use MediaWiki\User\UserIdentityValue;
13 use MediaWikiTestCase;
14 use ParserOptions;
15 use ParserOutput;
16 use PHPUnit\Framework\MockObject\MockObject;
17 use Title;
18 use User;
19 use Wikimedia\Rdbms\IDatabase;
20 use Wikimedia\Rdbms\ILoadBalancer;
21 use WikitextContent;
22
23 /**
24 * @covers \MediaWiki\Revision\RevisionRenderer
25 */
26 class RevisionRendererTest extends MediaWikiTestCase {
27
28 /**
29 * @param $articleId
30 * @param $revisionId
31 * @return Title
32 */
33 private function getMockTitle( $articleId, $revisionId ) {
34 /** @var Title|MockObject $mock */
35 $mock = $this->getMockBuilder( Title::class )
36 ->disableOriginalConstructor()
37 ->getMock();
38 $mock->expects( $this->any() )
39 ->method( 'getNamespace' )
40 ->will( $this->returnValue( NS_MAIN ) );
41 $mock->expects( $this->any() )
42 ->method( 'getText' )
43 ->will( $this->returnValue( __CLASS__ ) );
44 $mock->expects( $this->any() )
45 ->method( 'getPrefixedText' )
46 ->will( $this->returnValue( __CLASS__ ) );
47 $mock->expects( $this->any() )
48 ->method( 'getDBkey' )
49 ->will( $this->returnValue( __CLASS__ ) );
50 $mock->expects( $this->any() )
51 ->method( 'getArticleID' )
52 ->will( $this->returnValue( $articleId ) );
53 $mock->expects( $this->any() )
54 ->method( 'getLatestRevId' )
55 ->will( $this->returnValue( $revisionId ) );
56 $mock->expects( $this->any() )
57 ->method( 'getContentModel' )
58 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
59 $mock->expects( $this->any() )
60 ->method( 'getPageLanguage' )
61 ->will( $this->returnValue( Language::factory( 'en' ) ) );
62 $mock->expects( $this->any() )
63 ->method( 'isContentPage' )
64 ->will( $this->returnValue( true ) );
65 $mock->expects( $this->any() )
66 ->method( 'equals' )
67 ->willReturnCallback(
68 function ( Title $other ) use ( $mock ) {
69 return $mock->getArticleID() === $other->getArticleID();
70 }
71 );
72 $mock->expects( $this->any() )
73 ->method( 'userCan' )
74 ->willReturnCallback(
75 function ( $perm, User $user ) use ( $mock ) {
76 return $user->isAllowed( $perm );
77 }
78 );
79
80 return $mock;
81 }
82
83 /**
84 * @param int $maxRev
85 * @param int $linkCount
86 *
87 * @return IDatabase
88 */
89 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
90 /** @var IDatabase|MockObject $db */
91 $db = $this->getMock( IDatabase::class );
92 $db->method( 'selectField' )
93 ->willReturnCallback(
94 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
95 return $this->selectFieldCallback(
96 $table,
97 $fields,
98 $cond,
99 $maxRev,
100 $linkCount
101 );
102 }
103 );
104
105 return $db;
106 }
107
108 /**
109 * @return RevisionRenderer
110 */
111 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
112 $dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
113
114 $db = $this->getMockDatabaseConnection( $maxRev );
115
116 /** @var ILoadBalancer|MockObject $lb */
117 $lb = $this->getMock( ILoadBalancer::class );
118 $lb->method( 'getConnection' )
119 ->with( $dbIndex )
120 ->willReturn( $db );
121 $lb->method( 'getConnectionRef' )
122 ->with( $dbIndex )
123 ->willReturn( $db );
124 $lb->method( 'getLazyConnectionRef' )
125 ->with( $dbIndex )
126 ->willReturn( $db );
127
128 return new RevisionRenderer( $lb );
129 }
130
131 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
132 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
133 return $maxRev;
134 }
135
136 $this->fail( 'Unexpected call to selectField' );
137 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
138 }
139
140 public function testGetRenderedRevision_new() {
141 $renderer = $this->newRevisionRenderer( 100 );
142 $title = $this->getMockTitle( 7, 21 );
143
144 $rev = new MutableRevisionRecord( $title );
145 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
146 $rev->setTimestamp( '20180101000003' );
147 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
148
149 $text = "";
150 $text .= "* page:{{PAGENAME}}\n";
151 $text .= "* rev:{{REVISIONID}}\n";
152 $text .= "* user:{{REVISIONUSER}}\n";
153 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
154 $text .= "* [[Link It]]\n";
155
156 $rev->setContent( 'main', new WikitextContent( $text ) );
157
158 $options = ParserOptions::newCanonical( 'canonical' );
159 $rr = $renderer->getRenderedRevision( $rev, $options );
160
161 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
162
163 $this->assertSame( $rev, $rr->getRevision() );
164 $this->assertSame( $options, $rr->getOptions() );
165
166 $html = $rr->getRevisionParserOutput()->getText();
167
168 $this->assertContains( 'page:' . __CLASS__, $html );
169 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
170 $this->assertContains( 'user:Frank', $html );
171 $this->assertContains( 'time:20180101000003', $html );
172
173 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
174 }
175
176 public function testGetRenderedRevision_current() {
177 $renderer = $this->newRevisionRenderer( 100 );
178 $title = $this->getMockTitle( 7, 21 );
179
180 $rev = new MutableRevisionRecord( $title );
181 $rev->setId( 21 ); // current!
182 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
183 $rev->setTimestamp( '20180101000003' );
184 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
185
186 $text = "";
187 $text .= "* page:{{PAGENAME}}\n";
188 $text .= "* rev:{{REVISIONID}}\n";
189 $text .= "* user:{{REVISIONUSER}}\n";
190 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
191
192 $rev->setContent( 'main', new WikitextContent( $text ) );
193
194 $options = ParserOptions::newCanonical( 'canonical' );
195 $rr = $renderer->getRenderedRevision( $rev, $options );
196
197 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
198
199 $this->assertSame( $rev, $rr->getRevision() );
200 $this->assertSame( $options, $rr->getOptions() );
201
202 $html = $rr->getRevisionParserOutput()->getText();
203
204 $this->assertContains( 'page:' . __CLASS__, $html );
205 $this->assertContains( 'rev:21', $html );
206 $this->assertContains( 'user:Frank', $html );
207 $this->assertContains( 'time:20180101000003', $html );
208
209 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
210 }
211
212 public function testGetRenderedRevision_master() {
213 $renderer = $this->newRevisionRenderer( 100, true ); // use master
214 $title = $this->getMockTitle( 7, 21 );
215
216 $rev = new MutableRevisionRecord( $title );
217 $rev->setId( 21 ); // current!
218 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
219 $rev->setTimestamp( '20180101000003' );
220 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
221
222 $text = "";
223 $text .= "* page:{{PAGENAME}}\n";
224 $text .= "* rev:{{REVISIONID}}\n";
225 $text .= "* user:{{REVISIONUSER}}\n";
226 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
227
228 $rev->setContent( 'main', new WikitextContent( $text ) );
229
230 $options = ParserOptions::newCanonical( 'canonical' );
231 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
232
233 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
234
235 $html = $rr->getRevisionParserOutput()->getText();
236
237 $this->assertContains( 'rev:21', $html );
238
239 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
240 }
241
242 public function testGetRenderedRevision_old() {
243 $renderer = $this->newRevisionRenderer( 100 );
244 $title = $this->getMockTitle( 7, 21 );
245
246 $rev = new MutableRevisionRecord( $title );
247 $rev->setId( 11 ); // old!
248 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
249 $rev->setTimestamp( '20180101000003' );
250 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
251
252 $text = "";
253 $text .= "* page:{{PAGENAME}}\n";
254 $text .= "* rev:{{REVISIONID}}\n";
255 $text .= "* user:{{REVISIONUSER}}\n";
256 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
257
258 $rev->setContent( 'main', new WikitextContent( $text ) );
259
260 $options = ParserOptions::newCanonical( 'canonical' );
261 $rr = $renderer->getRenderedRevision( $rev, $options );
262
263 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
264
265 $this->assertSame( $rev, $rr->getRevision() );
266 $this->assertSame( $options, $rr->getOptions() );
267
268 $html = $rr->getRevisionParserOutput()->getText();
269
270 $this->assertContains( 'page:' . __CLASS__, $html );
271 $this->assertContains( 'rev:11', $html );
272 $this->assertContains( 'user:Frank', $html );
273 $this->assertContains( 'time:20180101000003', $html );
274
275 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
276 }
277
278 public function testGetRenderedRevision_suppressed() {
279 $renderer = $this->newRevisionRenderer( 100 );
280 $title = $this->getMockTitle( 7, 21 );
281
282 $rev = new MutableRevisionRecord( $title );
283 $rev->setId( 11 ); // old!
284 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
285 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
286 $rev->setTimestamp( '20180101000003' );
287 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
288
289 $text = "";
290 $text .= "* page:{{PAGENAME}}\n";
291 $text .= "* rev:{{REVISIONID}}\n";
292 $text .= "* user:{{REVISIONUSER}}\n";
293 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
294
295 $rev->setContent( 'main', new WikitextContent( $text ) );
296
297 $options = ParserOptions::newCanonical( 'canonical' );
298 $rr = $renderer->getRenderedRevision( $rev, $options );
299
300 $this->assertNull( $rr, 'getRenderedRevision' );
301 }
302
303 public function testGetRenderedRevision_privileged() {
304 $renderer = $this->newRevisionRenderer( 100 );
305 $title = $this->getMockTitle( 7, 21 );
306
307 $rev = new MutableRevisionRecord( $title );
308 $rev->setId( 11 ); // old!
309 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
310 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
311 $rev->setTimestamp( '20180101000003' );
312 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
313
314 $text = "";
315 $text .= "* page:{{PAGENAME}}\n";
316 $text .= "* rev:{{REVISIONID}}\n";
317 $text .= "* user:{{REVISIONUSER}}\n";
318 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
319
320 $rev->setContent( 'main', new WikitextContent( $text ) );
321
322 $options = ParserOptions::newCanonical( 'canonical' );
323 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
324 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
325
326 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
327
328 $this->assertSame( $rev, $rr->getRevision() );
329 $this->assertSame( $options, $rr->getOptions() );
330
331 $html = $rr->getRevisionParserOutput()->getText();
332
333 // Suppressed content should be visible for sysops
334 $this->assertContains( 'page:' . __CLASS__, $html );
335 $this->assertContains( 'rev:11', $html );
336 $this->assertContains( 'user:Frank', $html );
337 $this->assertContains( 'time:20180101000003', $html );
338
339 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
340 }
341
342 public function testGetRenderedRevision_raw() {
343 $renderer = $this->newRevisionRenderer( 100 );
344 $title = $this->getMockTitle( 7, 21 );
345
346 $rev = new MutableRevisionRecord( $title );
347 $rev->setId( 11 ); // old!
348 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
349 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
350 $rev->setTimestamp( '20180101000003' );
351 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
352
353 $text = "";
354 $text .= "* page:{{PAGENAME}}\n";
355 $text .= "* rev:{{REVISIONID}}\n";
356 $text .= "* user:{{REVISIONUSER}}\n";
357 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
358
359 $rev->setContent( 'main', new WikitextContent( $text ) );
360
361 $options = ParserOptions::newCanonical( 'canonical' );
362 $rr = $renderer->getRenderedRevision(
363 $rev,
364 $options,
365 null,
366 [ 'audience' => RevisionRecord::RAW ]
367 );
368
369 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
370
371 $this->assertSame( $rev, $rr->getRevision() );
372 $this->assertSame( $options, $rr->getOptions() );
373
374 $html = $rr->getRevisionParserOutput()->getText();
375
376 // Suppressed content should be visible in raw mode
377 $this->assertContains( 'page:' . __CLASS__, $html );
378 $this->assertContains( 'rev:11', $html );
379 $this->assertContains( 'user:Frank', $html );
380 $this->assertContains( 'time:20180101000003', $html );
381
382 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
383 }
384
385 public function testGetRenderedRevision_multi() {
386 $renderer = $this->newRevisionRenderer();
387 $title = $this->getMockTitle( 7, 21 );
388
389 $rev = new MutableRevisionRecord( $title );
390 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
391 $rev->setTimestamp( '20180101000003' );
392 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
393
394 $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
395 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
396
397 $rr = $renderer->getRenderedRevision( $rev );
398
399 $combinedOutput = $rr->getRevisionParserOutput();
400 $mainOutput = $rr->getSlotParserOutput( 'main' );
401 $auxOutput = $rr->getSlotParserOutput( 'aux' );
402
403 $combinedHtml = $combinedOutput->getText();
404 $mainHtml = $mainOutput->getText();
405 $auxHtml = $auxOutput->getText();
406
407 $this->assertContains( 'Kittens', $mainHtml );
408 $this->assertContains( 'Goats', $auxHtml );
409 $this->assertNotContains( 'Goats', $mainHtml );
410 $this->assertNotContains( 'Kittens', $auxHtml );
411 $this->assertContains( 'Kittens', $combinedHtml );
412 $this->assertContains( 'Goats', $combinedHtml );
413 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
414 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
415
416 // make sure output wrapping works right
417 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
418 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
419 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
420
421 // there should be only one wrapper div
422 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
423 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
424
425 $combinedLinks = $combinedOutput->getLinks();
426 $mainLinks = $mainOutput->getLinks();
427 $auxLinks = $auxOutput->getLinks();
428 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
429 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
430 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
431 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
432 }
433
434 public function testGetRenderedRevision_noHtml() {
435 /** @var MockObject|Content $mockContent */
436 $mockContent = $this->getMockBuilder( WikitextContent::class )
437 ->setMethods( [ 'getParserOutput' ] )
438 ->setConstructorArgs( [ 'Whatever' ] )
439 ->getMock();
440 $mockContent->method( 'getParserOutput' )
441 ->willReturnCallback( function ( Title $title, $revId = null,
442 ParserOptions $options = null, $generateHtml = true
443 ) {
444 if ( !$generateHtml ) {
445 return new ParserOutput( null );
446 } else {
447 $this->fail( 'Should not be called with $generateHtml == true' );
448 return null; // never happens, make analyzer happy
449 }
450 } );
451
452 $renderer = $this->newRevisionRenderer();
453 $title = $this->getMockTitle( 7, 21 );
454
455 $rev = new MutableRevisionRecord( $title );
456 $rev->setContent( 'main', $mockContent );
457 $rev->setContent( 'aux', $mockContent );
458
459 // NOTE: we are testing the private combineSlotOutput() callback here.
460 $rr = $renderer->getRenderedRevision( $rev );
461
462 $output = $rr->getSlotParserOutput( 'main', [ 'generate-html' => false ] );
463 $this->assertFalse( $output->hasText(), 'hasText' );
464
465 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
466 $this->assertFalse( $output->hasText(), 'hasText' );
467 }
468
469 }