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
344 lines
8.7 KiB
PHP
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 ) );
|
|
}
|
|
}
|