wiki.techinc.nl/tests/phpunit/includes/Revision/RevisionStoreTest.php
Tim Starling 088a313fec ContribsPager row filtering with RevisionStore::isRevisionRow
Flow hooks into ContribsPager, causing formatRow() to be called with
FormatterRow objects instead of stdClass objects. formatRow() is
expected to silently decline to format such objects, leaving formatting
up to a subsequent hook.

Instead of calling newRevisionFromRow with all warnings suppressed and
all exceptions caught, provide isRevisionRow() which determines whether
the row is valid. Thus, unexpected exceptions will be visible and the
code does not depend on details of how newRevisionFromRow() validates
its arguments.

Bug: T288563
Change-Id: Id0316886d770cd905897d515b3eb658a5875bd80
2021-08-10 17:59:30 -07:00

344 lines
8.7 KiB
PHP

<?php
namespace MediaWiki\Tests\Revision;
use MediaWiki\Revision\RevisionAccessException;
use MediaWiki\Revision\RevisionStore;
use MediaWikiIntegrationTestCase;
use MWTimestamp;
use PHPUnit\Framework\MockObject\MockObject;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\MaintainableDBConnRef;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* Tests RevisionStore
*/
class RevisionStoreTest extends MediaWikiIntegrationTestCase {
/**
* @return RevisionStore
*/
private function getRevisionStore() {
return $this->getServiceContainer()->getRevisionStore();
}
/**
* @param IDatabase $db
*
* @return MockObject|ILoadBalancer
*/
private function installMockLoadBalancer( IDatabase $db ) {
$lb = $this->createNoOpMock( ILoadBalancer::class, [ 'getConnectionRef', 'getLocalDomainID' ] );
$dbRef = new MaintainableDBConnRef( $lb, $db, DB_PRIMARY );
$lb->method( 'getConnectionRef' )->willReturn( $dbRef );
$lb->method( 'getLocalDomainID' )->willReturn( 'fake' );
$lbf = $this->createNoOpMock( LBFactory::class, [ 'getMainLB', 'getLocalDomainID' ] );
$lbf->method( 'getMainLB' )->willReturn( $lb );
$lbf->method( 'getLocalDomainID' )->willReturn( 'fake' );
$this->setService( 'DBLoadBalancerFactory', $lbf );
return $lb;
}
/**
* @return MockObject|IDatabase
*/
private function installMockDatabase() {
$db = $this->getMockBuilder( IDatabase::class )
->disableAutoReturnValueGeneration()
->disableOriginalConstructor()->getMock();
$this->installMockLoadBalancer( $db );
return $db;
}
/**
* @return MockObject|IDatabase
*/
private function getMockDatabase() {
return $this->getMockBuilder( IDatabase::class )
->disableOriginalConstructor()->getMock();
}
private function getDummyPageRow( $extra = [] ) {
return (object)( $extra + [
'page_id' => 1337,
'page_namespace' => 0,
'page_title' => 'Test',
'page_is_redirect' => 0,
'page_is_new' => 0,
'page_touched' => MWTimestamp::now(),
'page_links_updated' => MWTimestamp::now(),
'page_latest' => 23948576,
'page_len' => 2323,
'page_content_model' => CONTENT_MODEL_WIKITEXT
] );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getTitle
*/
public function testGetTitle_successFromPageId() {
$db = $this->installMockDatabase();
// First query is by page ID. Return result
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( $this->getDummyPageRow( [
'page_id' => '1',
'page_namespace' => '3',
'page_title' => 'Food',
] ) );
$db->method( 'selectRow' )
->willReturn( false );
$store = $this->getRevisionStore();
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 3, $title->getNamespace() );
$this->assertSame( 'Food', $title->getDBkey() );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getTitle
*/
public function testGetTitle_successFromPageIdOnFallback() {
$db = $this->installMockDatabase();
// First query, by page_id, no result
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// Second query, by rev_id, no result
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 0 => 'page', 'revision' => 'revision' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
// Retrying on master...
// Third query, by page_id again
$db->expects( $this->at( 2 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( $this->getDummyPageRow( [
'page_namespace' => '2',
'page_title' => 'Foodey',
] ) );
$store = $this->getRevisionStore();
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 2, $title->getNamespace() );
$this->assertSame( 'Foodey', $title->getDBkey() );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getTitle
*/
public function testGetTitle_successFromRevId() {
$db = $this->installMockDatabase();
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// Second select using rev_id, faking no result (db lag?)
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 0 => 'page', 'revision' => 'revision' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( $this->getDummyPageRow( [
'page_namespace' => '1',
'page_title' => 'Food2',
] ) );
$store = $this->getRevisionStore();
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 1, $title->getNamespace() );
$this->assertSame( 'Food2', $title->getDBkey() );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getTitle
*/
public function testGetTitle_successFromRevIdOnFallback() {
$db = $this->installMockDatabase();
// First query, by page_id, no result
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// Second query, by rev_id, no result
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 0 => 'page', 'revision' => 'revision' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
// Retrying on master...
// Third query, by page_id again, still no result
$db->expects( $this->at( 2 ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// Forth query, by rev_id agin
$db->expects( $this->at( 3 ) )
->method( 'selectRow' )
->with(
[ 0 => 'page', 'revision' => 'revision' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( $this->getDummyPageRow( [
'page_namespace' => '2',
'page_title' => 'Foodey',
] ) );
$store = $this->getRevisionStore();
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 2, $title->getNamespace() );
$this->assertSame( 'Foodey', $title->getDBkey() );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getTitle
*/
public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
$db = $this->getMockDatabase();
$mockLoadBalancer = $this->installMockLoadBalancer( $db );
// Assert that the first call uses a REPLICA and the second falls back to master
// RevisionStore getTitle uses getConnectionRef
$mockLoadBalancer->expects( $this->exactly( 4 ) )
->method( 'getConnectionRef' )
->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
static $callCounter = 0;
$callCounter++;
// The first call should be to a REPLICA, and the second a MASTER.
if ( $callCounter < 3 ) {
$this->assertSame( DB_REPLICA, $masterOrReplica );
} else {
$this->assertSame( DB_PRIMARY, $masterOrReplica );
}
return $db;
} );
// First and third call to Title::newFromID, faking no result
foreach ( [ 0, 2 ] as $counter ) {
$db->expects( $this->at( $counter ) )
->method( 'selectRow' )
->with(
[ 'page' ],
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
}
foreach ( [ 1, 3 ] as $counter ) {
$db->expects( $this->at( $counter ) )
->method( 'selectRow' )
->with(
[ 0 => 'page', 'revision' => 'revision' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
}
$store = $this->getRevisionStore( $mockLoadBalancer );
$this->expectException( RevisionAccessException::class );
$store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
}
public function provideIsRevisionRow() {
yield 'invalid row type' => [
'row' => new class() {
},
'expect' => false,
];
yield 'invalid row' => [
'row' => (object)[ 'blabla' => 'bla' ],
'expect' => false,
];
yield 'valid row' => [
'row' => (object)[
'rev_id' => 321,
'rev_page' => 123,
'rev_timestamp' => ConvertibleTimestamp::now(),
'rev_minor_edit' => 0,
'rev_deleted' => 0,
'rev_len' => 10,
'rev_parent_id' => 123,
'rev_sha1' => 'abc',
'rev_comment_text' => 'blabla',
'rev_comment_data' => 'blablabla',
'rev_comment_cid' => 1,
'rev_actor' => 1,
'rev_user' => 1,
'rev_user_text' => 'alala',
],
'expect' => true,
];
}
/**
* @covers \MediaWiki\Storage\RevisionStore::isRevisionRow
* @dataProvider provideIsRevisionRow
*/
public function testIsRevisionRow( $row, bool $expect ) {
$this->assertSame( $expect, $this->getRevisionStore()->isRevisionRow( $row ) );
}
}