The new getPage() handles cross-wiki access correctly. It's not a direct replacement for getTitle(), since it is private. getTitle() cannot work properly for RevisionStore instances connected to a sister-wiki. It was already discouraged, and is nearly unused outside of RevisionStore. Remainign usages should be replaced with RevisionRecord::getPage(). Note that PageIdentityValue is not lazy loading, while Title is. Code in newRevisionFromRowAndSlots() was adjusted to take advantage of any fields from the page table that may already be present from a join, to avoid an additional query to the page table. Bug: T275531 Bug: T273284 Change-Id: If4525d76a578b92dbcfe1f5150028f7a28811119
347 lines
9.8 KiB
PHP
347 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Revision;
|
|
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Revision\RevisionAccessException;
|
|
use MediaWiki\Revision\RevisionStore;
|
|
use MediaWiki\Storage\SqlBlobStore;
|
|
use MediaWikiIntegrationTestCase;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use WANObjectCache;
|
|
use Wikimedia\Rdbms\IDatabase;
|
|
use Wikimedia\Rdbms\ILoadBalancer;
|
|
use Wikimedia\Rdbms\LoadBalancer;
|
|
use Wikimedia\Rdbms\MaintainableDBConnRef;
|
|
|
|
/**
|
|
* Tests RevisionStore
|
|
*/
|
|
class RevisionStoreTest extends MediaWikiIntegrationTestCase {
|
|
|
|
/**
|
|
* @param LoadBalancer|null $loadBalancer
|
|
* @param SqlBlobStore|null $blobStore
|
|
* @param WANObjectCache|null $WANObjectCache
|
|
*
|
|
* @return RevisionStore
|
|
*/
|
|
private function getRevisionStore(
|
|
$loadBalancer = null,
|
|
$blobStore = null,
|
|
$WANObjectCache = null
|
|
) {
|
|
return new RevisionStore(
|
|
$loadBalancer ?: $this->getMockLoadBalancer(),
|
|
$blobStore ?: $this->getMockSqlBlobStore(),
|
|
$WANObjectCache ?: $this->getHashWANObjectCache(),
|
|
MediaWikiServices::getInstance()->getCommentStore(),
|
|
MediaWikiServices::getInstance()->getContentModelStore(),
|
|
MediaWikiServices::getInstance()->getSlotRoleStore(),
|
|
MediaWikiServices::getInstance()->getSlotRoleRegistry(),
|
|
MediaWikiServices::getInstance()->getActorMigration(),
|
|
MediaWikiServices::getInstance()->getActorStore(),
|
|
$this->getMockContentHandlerFactory(),
|
|
MediaWikiServices::getInstance()->getTitleFactory(),
|
|
MediaWikiServices::getInstance()->getHookContainer()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return MockObject|LoadBalancer
|
|
*/
|
|
private function getMockLoadBalancer() {
|
|
return $this->getMockBuilder( LoadBalancer::class )
|
|
->disableOriginalConstructor()->getMock();
|
|
}
|
|
|
|
/**
|
|
* @return MockObject|IDatabase
|
|
*/
|
|
private function getMockDatabase() {
|
|
return $this->getMockBuilder( IDatabase::class )
|
|
->disableOriginalConstructor()->getMock();
|
|
}
|
|
|
|
/**
|
|
* @param ILoadBalancer $mockLoadBalancer
|
|
* @param Database $db
|
|
* @return callable
|
|
*/
|
|
private function getMockDBConnRefCallback( ILoadBalancer $mockLoadBalancer, IDatabase $db ) {
|
|
return static function ( $i, $g, $domain, $flg ) use ( $mockLoadBalancer, $db ) {
|
|
return new MaintainableDBConnRef( $mockLoadBalancer, $db, $i );
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return MockObject|SqlBlobStore
|
|
*/
|
|
private function getMockSqlBlobStore() {
|
|
return $this->getMockBuilder( SqlBlobStore::class )
|
|
->disableOriginalConstructor()->getMock();
|
|
}
|
|
|
|
private function getHashWANObjectCache() {
|
|
return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
|
|
}
|
|
|
|
/**
|
|
* @return IContentHandlerFactory|MockObject
|
|
*/
|
|
public function getMockContentHandlerFactory(): IContentHandlerFactory {
|
|
return $this->createMock( IContentHandlerFactory::class );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Revision\RevisionStore::getTitle
|
|
*/
|
|
public function testGetTitle_successFromPageId() {
|
|
$mockLoadBalancer = $this->getMockLoadBalancer();
|
|
// Title calls wfGetDB() so we have to set the main service
|
|
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
|
|
|
|
$db = $this->getMockDatabase();
|
|
// RevisionStore uses getConnectionRef
|
|
$mockLoadBalancer->expects( $this->any() )
|
|
->method( 'getConnectionRef' )
|
|
->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
|
|
|
|
// 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( (object)[
|
|
'page_namespace' => '1',
|
|
'page_title' => 'Food',
|
|
] );
|
|
|
|
$store = $this->getRevisionStore( $mockLoadBalancer );
|
|
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
|
|
|
|
$this->assertSame( 1, $title->getNamespace() );
|
|
$this->assertSame( 'Food', $title->getDBkey() );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\Revision\RevisionStore::getTitle
|
|
*/
|
|
public function testGetTitle_successFromPageIdOnFallback() {
|
|
$mockLoadBalancer = $this->getMockLoadBalancer();
|
|
// Title calls wfGetDB() so we have to set the main service
|
|
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
|
|
|
|
$db = $this->getMockDatabase();
|
|
// Assert that the first call uses a REPLICA and the second falls back to master
|
|
$mockLoadBalancer->expects( $this->atLeastOnce() )
|
|
->method( 'getConnectionRef' )
|
|
->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
|
|
|
|
// 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 );
|
|
|
|
// First select using rev_id, faking no result (db lag?)
|
|
$db->expects( $this->at( 1 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
[ 'revision', 'page' ],
|
|
$this->anything(),
|
|
[ 'rev_id' => 2 ]
|
|
)
|
|
->willReturn( false );
|
|
|
|
// Second call to Title::newFromID, no result
|
|
$db->expects( $this->at( 2 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
'page',
|
|
$this->anything(),
|
|
[ 'page_id' => 1 ]
|
|
)
|
|
->willReturn( (object)[
|
|
'page_namespace' => '2',
|
|
'page_title' => 'Foodey',
|
|
] );
|
|
|
|
$store = $this->getRevisionStore( $mockLoadBalancer );
|
|
$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() {
|
|
$mockLoadBalancer = $this->getMockLoadBalancer();
|
|
// Title calls wfGetDB() so we have to set the main service
|
|
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
|
|
|
|
$db = $this->getMockDatabase();
|
|
$mockLoadBalancer->expects( $this->atLeastOnce() )
|
|
->method( 'getConnectionRef' )
|
|
->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
|
|
|
|
// 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 );
|
|
|
|
// First select using rev_id, faking no result (db lag?)
|
|
$db->expects( $this->at( 1 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
[ 'revision', 'page' ],
|
|
$this->anything(),
|
|
[ 'rev_id' => 2 ]
|
|
)
|
|
->willReturn( (object)[
|
|
'page_namespace' => '1',
|
|
'page_title' => 'Food2',
|
|
] );
|
|
|
|
$store = $this->getRevisionStore( $mockLoadBalancer );
|
|
$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() {
|
|
$mockLoadBalancer = $this->getMockLoadBalancer();
|
|
// Title calls wfGetDB() so we have to set the main service
|
|
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
|
|
|
|
$db = $this->getMockDatabase();
|
|
// Assert that the first call uses a REPLICA and the second falls back to master
|
|
$mockLoadBalancer->expects( $this->atLeastOnce() )
|
|
->method( 'getConnectionRef' )
|
|
->willReturnCallback( $this->getMockDBConnRefCallback( $mockLoadBalancer, $db ) );
|
|
|
|
// 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 );
|
|
|
|
// First select using rev_id, faking no result (db lag?)
|
|
$db->expects( $this->at( 1 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
[ 'revision', 'page' ],
|
|
$this->anything(),
|
|
[ 'rev_id' => 2 ]
|
|
)
|
|
->willReturn( false );
|
|
|
|
// Second call to Title::newFromID, no result
|
|
$db->expects( $this->at( 2 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
'page',
|
|
$this->anything(),
|
|
[ 'page_id' => 1 ]
|
|
)
|
|
->willReturn( false );
|
|
|
|
// Second select using rev_id, result
|
|
$db->expects( $this->at( 3 ) )
|
|
->method( 'selectRow' )
|
|
->with(
|
|
[ 'revision', 'page' ],
|
|
$this->anything(),
|
|
[ 'rev_id' => 2 ]
|
|
)
|
|
->willReturn( (object)[
|
|
'page_namespace' => '2',
|
|
'page_title' => 'Foodey',
|
|
] );
|
|
|
|
$store = $this->getRevisionStore( $mockLoadBalancer );
|
|
$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() {
|
|
$mockLoadBalancer = $this->getMockLoadBalancer();
|
|
// Title calls wfGetDB() so we have to set the main service
|
|
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
|
|
|
|
$db = $this->getMockDatabase();
|
|
// 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_MASTER, $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(
|
|
[ 'revision', 'page' ],
|
|
$this->anything(),
|
|
[ 'rev_id' => 2 ]
|
|
)
|
|
->willReturn( false );
|
|
}
|
|
|
|
$store = $this->getRevisionStore( $mockLoadBalancer );
|
|
|
|
$this->expectException( RevisionAccessException::class );
|
|
$store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
|
|
}
|
|
|
|
}
|