wiki.techinc.nl/tests/phpunit/includes/Revision/RevisionRendererTest.php
Brad Jorsch dff469a408 Re-namespace RevisionStore and RevisionRecord classes
During development a lot of classes were placed in MediaWiki\Storage\.
The precedent set would mean that every class relating to something
stored in a database table, plus all related value classes and such,
would go into that namespace.

Let's put them into MediaWiki\Revision\ instead. Then future classes
related to the 'page' table can go into MediaWiki\Page\, future classes
related to the 'user' table can go into MediaWiki\User\, and so on.

Note I didn't move DerivedPageDataUpdater, PageUpdateException,
PageUpdater, or RevisionSlotsUpdate in this patch. If these are kept
long-term, they probably belong in MediaWiki\Page\ or MediaWiki\Edit\
instead.

Bug: T204158
Change-Id: I16bea8927566a3c73c07e4f4afb3537e05aa04a5
2018-10-09 10:22:48 -04:00

470 lines
16 KiB
PHP

<?php
namespace MediaWiki\Tests\Revision;
use CommentStoreComment;
use Content;
use Language;
use LogicException;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
use MediaWiki\Revision\SlotRecord;
use MediaWikiTestCase;
use MediaWiki\User\UserIdentityValue;
use ParserOptions;
use ParserOutput;
use PHPUnit\Framework\MockObject\MockObject;
use Title;
use User;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use WikitextContent;
/**
* @covers \MediaWiki\Revision\RevisionRenderer
*/
class RevisionRendererTest extends MediaWikiTestCase {
/**
* @param $articleId
* @param $revisionId
* @return Title
*/
private function getMockTitle( $articleId, $revisionId ) {
/** @var Title|MockObject $mock */
$mock = $this->getMockBuilder( Title::class )
->disableOriginalConstructor()
->getMock();
$mock->expects( $this->any() )
->method( 'getNamespace' )
->will( $this->returnValue( NS_MAIN ) );
$mock->expects( $this->any() )
->method( 'getText' )
->will( $this->returnValue( __CLASS__ ) );
$mock->expects( $this->any() )
->method( 'getPrefixedText' )
->will( $this->returnValue( __CLASS__ ) );
$mock->expects( $this->any() )
->method( 'getDBkey' )
->will( $this->returnValue( __CLASS__ ) );
$mock->expects( $this->any() )
->method( 'getArticleID' )
->will( $this->returnValue( $articleId ) );
$mock->expects( $this->any() )
->method( 'getLatestRevId' )
->will( $this->returnValue( $revisionId ) );
$mock->expects( $this->any() )
->method( 'getContentModel' )
->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
$mock->expects( $this->any() )
->method( 'getPageLanguage' )
->will( $this->returnValue( Language::factory( 'en' ) ) );
$mock->expects( $this->any() )
->method( 'isContentPage' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'equals' )
->willReturnCallback(
function ( Title $other ) use ( $mock ) {
return $mock->getArticleID() === $other->getArticleID();
}
);
$mock->expects( $this->any() )
->method( 'userCan' )
->willReturnCallback(
function ( $perm, User $user ) use ( $mock ) {
return $user->isAllowed( $perm );
}
);
return $mock;
}
/**
* @param int $maxRev
* @param int $linkCount
*
* @return IDatabase
*/
private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
/** @var IDatabase|MockObject $db */
$db = $this->getMock( IDatabase::class );
$db->method( 'selectField' )
->willReturnCallback(
function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
return $this->selectFieldCallback(
$table,
$fields,
$cond,
$maxRev,
$linkCount
);
}
);
return $db;
}
/**
* @return RevisionRenderer
*/
private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
$dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
$db = $this->getMockDatabaseConnection( $maxRev );
/** @var ILoadBalancer|MockObject $lb */
$lb = $this->getMock( ILoadBalancer::class );
$lb->method( 'getConnection' )
->with( $dbIndex )
->willReturn( $db );
$lb->method( 'getConnectionRef' )
->with( $dbIndex )
->willReturn( $db );
$lb->method( 'getLazyConnectionRef' )
->with( $dbIndex )
->willReturn( $db );
return new RevisionRenderer( $lb );
}
private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
return $maxRev;
}
$this->fail( 'Unexpected call to selectField' );
throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
}
public function testGetRenderedRevision_new() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$text .= "* [[Link It]]\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision( $rev, $options );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:' . __CLASS__, $html );
$this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
$this->assertContains( 'user:Frank', $html );
$this->assertContains( 'time:20180101000003', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRenderedRevision_current() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 21 ); // current!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision( $rev, $options );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:' . __CLASS__, $html );
$this->assertContains( 'rev:21', $html );
$this->assertContains( 'user:Frank', $html );
$this->assertContains( 'time:20180101000003', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRenderedRevision_master() {
$renderer = $this->newRevisionRenderer( 100, true ); // use master
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 21 ); // current!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'rev:21', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRenderedRevision_old() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 11 ); // old!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision( $rev, $options );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:' . __CLASS__, $html );
$this->assertContains( 'rev:11', $html );
$this->assertContains( 'user:Frank', $html );
$this->assertContains( 'time:20180101000003', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
}
public function testGetRenderedRevision_suppressed() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 11 ); // old!
$rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision( $rev, $options );
$this->assertNull( $rr, 'getRenderedRevision' );
}
public function testGetRenderedRevision_privileged() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 11 ); // old!
$rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
$rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
$this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
// Suppressed content should be visible for sysops
$this->assertContains( 'page:' . __CLASS__, $html );
$this->assertContains( 'rev:11', $html );
$this->assertContains( 'user:Frank', $html );
$this->assertContains( 'time:20180101000003', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
}
public function testGetRenderedRevision_raw() {
$renderer = $this->newRevisionRenderer( 100 );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 11 ); // old!
$rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$text = "";
$text .= "* page:{{PAGENAME}}\n";
$text .= "* rev:{{REVISIONID}}\n";
$text .= "* user:{{REVISIONUSER}}\n";
$text .= "* time:{{REVISIONTIMESTAMP}}\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = $renderer->getRenderedRevision(
$rev,
$options,
null,
[ 'audience' => RevisionRecord::RAW ]
);
$this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
// Suppressed content should be visible in raw mode
$this->assertContains( 'page:' . __CLASS__, $html );
$this->assertContains( 'rev:11', $html );
$this->assertContains( 'user:Frank', $html );
$this->assertContains( 'time:20180101000003', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
}
public function testGetRenderedRevision_multi() {
$renderer = $this->newRevisionRenderer();
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$rev->setTimestamp( '20180101000003' );
$rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
$rev->setContent( SlotRecord::MAIN, new WikitextContent( '[[Kittens]]' ) );
$rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
$rr = $renderer->getRenderedRevision( $rev );
$combinedOutput = $rr->getRevisionParserOutput();
$mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
$auxOutput = $rr->getSlotParserOutput( 'aux' );
$combinedHtml = $combinedOutput->getText();
$mainHtml = $mainOutput->getText();
$auxHtml = $auxOutput->getText();
$this->assertContains( 'Kittens', $mainHtml );
$this->assertContains( 'Goats', $auxHtml );
$this->assertNotContains( 'Goats', $mainHtml );
$this->assertNotContains( 'Kittens', $auxHtml );
$this->assertContains( 'Kittens', $combinedHtml );
$this->assertContains( 'Goats', $combinedHtml );
$this->assertContains( '>aux<', $combinedHtml, 'slot header' );
$this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
// make sure output wrapping works right
$this->assertContains( 'class="mw-parser-output"', $mainHtml );
$this->assertContains( 'class="mw-parser-output"', $auxHtml );
$this->assertContains( 'class="mw-parser-output"', $combinedHtml );
// there should be only one wrapper div
$this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
$this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
$combinedLinks = $combinedOutput->getLinks();
$mainLinks = $mainOutput->getLinks();
$auxLinks = $auxOutput->getLinks();
$this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
$this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
$this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
$this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
}
public function testGetRenderedRevision_noHtml() {
/** @var MockObject|Content $mockContent */
$mockContent = $this->getMockBuilder( WikitextContent::class )
->setMethods( [ 'getParserOutput' ] )
->setConstructorArgs( [ 'Whatever' ] )
->getMock();
$mockContent->method( 'getParserOutput' )
->willReturnCallback( function ( Title $title, $revId = null,
ParserOptions $options = null, $generateHtml = true
) {
if ( !$generateHtml ) {
return new ParserOutput( null );
} else {
$this->fail( 'Should not be called with $generateHtml == true' );
return null; // never happens, make analyzer happy
}
} );
$renderer = $this->newRevisionRenderer();
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setContent( SlotRecord::MAIN, $mockContent );
$rev->setContent( 'aux', $mockContent );
// NOTE: we are testing the private combineSlotOutput() callback here.
$rr = $renderer->getRenderedRevision( $rev );
$output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
$this->assertFalse( $output->hasText(), 'hasText' );
$output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
$this->assertFalse( $output->hasText(), 'hasText' );
}
}