A bit of a clean up. Switching out of lbFactory to ICP is not possible at the moment because there is $this->lbFactory->getLocalDomainID() for cache prefix. Fixable later. Bug: T330641 Change-Id: I2d4537f75f5877552c6d9fd2b76c5be1959ea400
3313 lines
92 KiB
PHP
3313 lines
92 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Cache\LinkBatchFactory;
|
|
use MediaWiki\Config\ServiceOptions;
|
|
use MediaWiki\Linker\LinksMigration;
|
|
use MediaWiki\Linker\LinkTarget;
|
|
use MediaWiki\Logger\LoggerFactory;
|
|
use MediaWiki\Page\PageIdentity;
|
|
use MediaWiki\Page\PageIdentityValue;
|
|
use MediaWiki\Revision\RevisionLookup;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Wikimedia\Rdbms\DBConnRef;
|
|
use Wikimedia\Rdbms\FakeResultWrapper;
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
|
use Wikimedia\Rdbms\LBFactory;
|
|
use Wikimedia\Rdbms\SelectQueryBuilder;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @author Addshore
|
|
* @author DannyS712
|
|
* @todo This test should become unittest again once LinksMigration is done (T300222)
|
|
*
|
|
* @covers WatchedItemStore
|
|
*/
|
|
class WatchedItemStoreUnitTest extends MediaWikiIntegrationTestCase {
|
|
use DummyServicesTrait;
|
|
use MockTitleTrait;
|
|
|
|
/**
|
|
* @return MockObject&DBConnRef
|
|
*/
|
|
private function getMockDb() {
|
|
$mock = $this->createMock( DBConnRef::class );
|
|
$mock->method( 'newSelectQueryBuilder' )
|
|
->willReturn( new SelectQueryBuilder( $mock ), new SelectQueryBuilder( $mock ), new SelectQueryBuilder( $mock ) );
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @param DBConnRef $mockDb
|
|
* @param string|null $expectedConnectionType
|
|
* @return MockObject&LBFactory
|
|
*/
|
|
private function getMockLBFactory(
|
|
DBConnRef $mockDb,
|
|
$expectedConnectionType = null
|
|
) {
|
|
$mock = $this->createMock( LBFactory::class );
|
|
$mock->method( 'getLocalDomainID' )
|
|
->willReturn( 'phpunitdb' );
|
|
$mock->method( 'getPrimaryDatabase' )
|
|
->willReturn( $mockDb );
|
|
$mock->method( 'getReplicaDatabase' )
|
|
->willReturn( $mockDb );
|
|
$mock->method( 'getLBsForOwner' )
|
|
->willReturn( [] );
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* The job queue is used in three different places - two "push" calls, and a
|
|
* "lazyPush" call - we don't test any of the "push" calls, so the callback
|
|
* can just run the job, but we do test the "lazyPush" call, and so the test
|
|
* that is using this may want to do something other than just run the job, since
|
|
* for ActivityUpdateJob instances this results in using global functions, which we
|
|
* cannot do in this unit test
|
|
*
|
|
* @param bool $mockLazyPush whether to add mock behavior for "lazyPush"
|
|
* @return MockObject|JobQueueGroup
|
|
*/
|
|
private function getMockJobQueueGroup( $mockLazyPush = true ) {
|
|
$mock = $this->createMock( JobQueueGroup::class );
|
|
$mock->method( 'push' )
|
|
->willReturnCallback( static function ( Job $job ) {
|
|
$job->run();
|
|
} );
|
|
if ( $mockLazyPush ) {
|
|
$mock->method( 'lazyPush' )
|
|
->willReturnCallback( static function ( Job $job ) {
|
|
$job->run();
|
|
} );
|
|
}
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @return MockObject|HashBagOStuff
|
|
*/
|
|
private function getMockCache() {
|
|
$mock = $this->getMockBuilder( HashBagOStuff::class )
|
|
->disableOriginalConstructor()
|
|
->onlyMethods( [ 'get', 'set', 'delete', 'makeKey' ] )
|
|
->getMock();
|
|
$mock->method( 'makeKey' )
|
|
->willReturnCallback( static function ( ...$args ) {
|
|
return implode( ':', $args );
|
|
} );
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* No methods may be called except provided callbacks, if any.
|
|
*
|
|
* @param array $callbacks Keys are method names, values are callbacks
|
|
* @param array $counts Keys are method names, values are expected number of times to be called
|
|
* (default is any number is okay)
|
|
* @return MockObject|RevisionLookup
|
|
*/
|
|
private function getMockRevisionLookup(
|
|
array $callbacks = [],
|
|
array $counts = []
|
|
): RevisionLookup {
|
|
$mock = $this->createNoOpMock( RevisionLookup::class, array_keys( $callbacks ) );
|
|
foreach ( $callbacks as $method => $callback ) {
|
|
$count = isset( $counts[$method] ) ? $this->exactly( $counts[$method] ) : $this->any();
|
|
$mock->expects( $count )
|
|
->method( $method )
|
|
->willReturnCallback( $callbacks[$method] );
|
|
}
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @param DBConnRef $mockDb
|
|
* @return LinkBatchFactory
|
|
*/
|
|
private function getMockLinkBatchFactory( DBConnRef $mockDb ) {
|
|
return new LinkBatchFactory(
|
|
$this->createMock( LinkCache::class ),
|
|
$this->createMock( TitleFormatter::class ),
|
|
$this->createMock( Language::class ),
|
|
$this->createMock( GenderCache::class ),
|
|
$this->getMockLBFactory( $mockDb ),
|
|
$this->createMock( LinksMigration::class ),
|
|
LoggerFactory::getInstance( 'LinkBatch' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array $mocks Associative array providing mocks to use when constructing the
|
|
* WatchedItemStore. Anything not provided will fall back to a default. Valid keys:
|
|
* * lbFactory
|
|
* * db
|
|
* * queueGroup
|
|
* * cache
|
|
* * readOnlyMode
|
|
* * nsInfo
|
|
* * revisionLookup
|
|
* * expiryEnabled
|
|
* * maxExpiryDuration
|
|
* * watchlistPurgeRate
|
|
* @return WatchedItemStore
|
|
*/
|
|
private function newWatchedItemStore( array $mocks = [] ): WatchedItemStore {
|
|
$options = new ServiceOptions( WatchedItemStore::CONSTRUCTOR_OPTIONS, [
|
|
'UpdateRowsPerQuery' => 1000,
|
|
'WatchlistExpiry' => $mocks['expiryEnabled'] ?? true,
|
|
'WatchlistExpiryMaxDuration' => $mocks['maxExpiryDuration'] ?? null,
|
|
'WatchlistPurgeRate' => $mocks['watchlistPurgeRate'] ?? 0.1,
|
|
] );
|
|
|
|
$db = $mocks['db'] ?? $this->getMockDb();
|
|
|
|
// If we don't use a manual mock for something specific, get a full
|
|
// NamespaceInfo service from DummyServicesTrait::getDummyNamespaceInfo
|
|
$nsInfo = $mocks['nsInfo'] ?? $this->getDummyNamespaceInfo();
|
|
|
|
return new WatchedItemStore(
|
|
$options,
|
|
$mocks['lbFactory'] ??
|
|
$this->getMockLBFactory( $db ),
|
|
$mocks['queueGroup'] ?? $this->getMockJobQueueGroup(),
|
|
$mocks['stash'] ?? new HashBagOStuff(),
|
|
$mocks['cache'] ?? $this->getMockCache(),
|
|
$mocks['readOnlyMode'] ?? $this->getDummyReadOnlyMode( false ),
|
|
$nsInfo,
|
|
$mocks['revisionLookup'] ?? $this->getMockRevisionLookup(),
|
|
$this->getMockLinkBatchFactory( $db )
|
|
);
|
|
}
|
|
|
|
public function testClearWatchedItems() {
|
|
$user = new UserIdentityValue( 7, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectField' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'COUNT(*)',
|
|
[
|
|
'wl_user' => $user->getId(),
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 12 );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with(
|
|
'watchlist',
|
|
[ 'wl_user' => 7 ],
|
|
$this->isType( 'string' )
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( 'RM-KEY' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false,
|
|
] );
|
|
TestingAccessWrapper::newFromObject( $store )
|
|
->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ];
|
|
|
|
$this->assertTrue( $store->clearUserWatchedItems( $user ) );
|
|
}
|
|
|
|
public function testClearWatchedItems_watchlistExpiry() {
|
|
$user = new UserIdentityValue( 7, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
// Select watchlist IDs.
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectFieldValues' )
|
|
->willReturn( [ 1, 2 ] );
|
|
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
[
|
|
'watchlist',
|
|
[ 'wl_id' => [ 1, 2 ] ]
|
|
],
|
|
[
|
|
'watchlist_expiry',
|
|
[ 'we_item' => [ 1, 2 ] ]
|
|
]
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( 'RM-KEY' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => true,
|
|
] );
|
|
TestingAccessWrapper::newFromObject( $store )
|
|
->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ];
|
|
|
|
$this->assertTrue( $store->clearUserWatchedItems( $user ) );
|
|
}
|
|
|
|
public function testClearWatchedItems_tooManyItemsWatched() {
|
|
$user = new UserIdentityValue( 7, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectField' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'COUNT(*)',
|
|
[
|
|
'wl_user' => $user->getId(),
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 99999 );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false,
|
|
] );
|
|
|
|
$this->assertFalse( $store->clearUserWatchedItems( $user ) );
|
|
}
|
|
|
|
public function testCountWatchedItems() {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectField' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
'COUNT(*)',
|
|
[
|
|
'wl_user' => $user->getId(),
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( '12' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals( 12, $store->countWatchedItems( $user ) );
|
|
}
|
|
|
|
public function provideTestPageFactory() {
|
|
yield [ static function ( $pageId, $namespace, $dbKey ) {
|
|
return new TitleValue( $namespace, $dbKey );
|
|
} ];
|
|
yield [ static function ( $pageId, $namespace, $dbKey ) {
|
|
return new PageIdentityValue( $pageId, $namespace, $dbKey, PageIdentityValue::LOCAL );
|
|
} ];
|
|
yield [ function ( $pageId, $namespace, $dbKey ) {
|
|
return $this->makeMockTitle( $dbKey, [
|
|
'id' => $pageId,
|
|
'namespace' => $namespace
|
|
] );
|
|
} ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testCountWatchers( $testPageFactory ) {
|
|
$titleValue = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectField' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
'COUNT(*)',
|
|
[
|
|
'wl_namespace' => $titleValue->getNamespace(),
|
|
'wl_title' => $titleValue->getDBkey(),
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( '7' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals( 7, $store->countWatchers( $titleValue ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testCountWatchersMultiple( $testPageFactory ) {
|
|
$titleValues = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 0, 'OtherDbKey' ),
|
|
$testPageFactory( 102, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
|
|
$dbResult = [
|
|
(object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ],
|
|
(object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ],
|
|
(object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ],
|
|
];
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
|
|
[
|
|
'makeWhereFrom2d return value',
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
],
|
|
$this->isType( 'string' ),
|
|
[
|
|
'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
|
|
]
|
|
)
|
|
->willReturn( $dbResult );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$expected = [
|
|
0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
|
|
1 => [ 'AnotherDbKey' => 500 ],
|
|
];
|
|
$this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) );
|
|
}
|
|
|
|
public static function provideIntWithDbUnsafeVersion() {
|
|
return [
|
|
[ 50 ],
|
|
[ "50; DROP TABLE watchlist;\n--" ],
|
|
];
|
|
}
|
|
|
|
public function provideTestPageFactoryAndIntWithDbUnsafeVersion() {
|
|
foreach ( $this->provideIntWithDbUnsafeVersion() as $dbint ) {
|
|
foreach ( $this->provideTestPageFactory() as $testPageFactory ) {
|
|
yield [ $dbint[0], $testPageFactory[0] ];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactoryAndIntWithDbUnsafeVersion
|
|
*/
|
|
public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers, $testPageFactory ) {
|
|
$titleValues = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 0, 'OtherDbKey' ),
|
|
$testPageFactory( 102, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
|
|
$dbResult = [
|
|
(object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ],
|
|
(object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ],
|
|
(object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ],
|
|
];
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
|
|
[
|
|
'makeWhereFrom2d return value',
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
],
|
|
$this->isType( 'string' ),
|
|
[
|
|
'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
|
|
'HAVING' => [ 'COUNT(*) >= 50' ],
|
|
]
|
|
)
|
|
->willReturn( $dbResult );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$expected = [
|
|
0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
|
|
1 => [ 'AnotherDbKey' => 500 ],
|
|
];
|
|
$this->assertEquals(
|
|
$expected,
|
|
$store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testCountVisitingWatchers( $testPageFactory ) {
|
|
$titleValue = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectField' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
'COUNT(*)',
|
|
[
|
|
'wl_namespace' => $titleValue->getNamespace(),
|
|
'wl_title' => $titleValue->getDBkey(),
|
|
'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL',
|
|
'we_expiry IS NULL OR we_expiry > \'20200101000000\''
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( '7' );
|
|
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'addQuotes' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
return "'$value'";
|
|
} );
|
|
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'timestamp' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
if ( $value === 0 ) {
|
|
return '20200101000000';
|
|
}
|
|
return 'TS' . $value . 'TS';
|
|
} );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testCountVisitingWatchersMultiple( $testPageFactory ) {
|
|
$titleValuesWithThresholds = [
|
|
[ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ],
|
|
[ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ],
|
|
[ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ],
|
|
];
|
|
|
|
$dbResult = [
|
|
(object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ],
|
|
(object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ],
|
|
(object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ],
|
|
];
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->exactly( 2 * 3 + 1 ) )
|
|
->method( 'addQuotes' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
return "'$value'";
|
|
} );
|
|
|
|
$mockDb->expects( $this->exactly( 4 ) )
|
|
->method( 'timestamp' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
if ( $value === 0 ) {
|
|
return '20200101000000';
|
|
}
|
|
return 'TS' . $value . 'TS';
|
|
} );
|
|
|
|
$mockDb->method( 'makeList' )
|
|
->with(
|
|
$this->isType( 'array' ),
|
|
$this->isType( 'int' )
|
|
)
|
|
->willReturnCallback( static function ( $a, $conj ) {
|
|
$sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
|
|
return implode( $sqlConj, array_map( static function ( $s ) {
|
|
return '(' . $s . ')';
|
|
}, $a
|
|
) );
|
|
} );
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'makeWhereFrom2d' );
|
|
|
|
$expectedCond =
|
|
'((wl_namespace = 0) AND (' .
|
|
"(((wl_title = 'SomeDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
')) OR (' .
|
|
"(wl_title = 'OtherDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
'))))' .
|
|
') OR ((wl_namespace = 1) AND (' .
|
|
"(((wl_title = 'AnotherDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
')))))';
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
|
|
[
|
|
$expectedCond,
|
|
'we_expiry IS NULL OR we_expiry > \'20200101000000\''
|
|
],
|
|
$this->isType( 'string' ),
|
|
[
|
|
'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
|
|
]
|
|
)
|
|
->willReturn( $dbResult );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$expected = [
|
|
0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
|
|
1 => [ 'AnotherDbKey' => 500 ],
|
|
];
|
|
$this->assertEquals(
|
|
$expected,
|
|
$store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testCountVisitingWatchersMultiple_withMissingTargets( $testPageFactory ) {
|
|
$titleValuesWithThresholds = [
|
|
[ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ],
|
|
[ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ],
|
|
[ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ],
|
|
[ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ],
|
|
[ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ],
|
|
];
|
|
|
|
$dbResult = [
|
|
(object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ],
|
|
(object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ],
|
|
(object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ],
|
|
(object)[ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '100' ],
|
|
(object)[ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '200' ],
|
|
];
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->exactly( 2 * 3 ) )
|
|
->method( 'addQuotes' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
return "'$value'";
|
|
} );
|
|
$mockDb->expects( $this->exactly( 3 ) )
|
|
->method( 'timestamp' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
return 'TS' . $value . 'TS';
|
|
} );
|
|
$mockDb->method( 'makeList' )
|
|
->with(
|
|
$this->isType( 'array' ),
|
|
$this->isType( 'int' )
|
|
)
|
|
->willReturnCallback( static function ( $a, $conj ) {
|
|
$sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
|
|
return implode( $sqlConj, array_map( static function ( $s ) {
|
|
return '(' . $s . ')';
|
|
}, $a
|
|
) );
|
|
} );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
|
|
$expectedCond =
|
|
'((wl_namespace = 0) AND (' .
|
|
"(((wl_title = 'SomeDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
')) OR (' .
|
|
"(wl_title = 'OtherDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
'))))' .
|
|
') OR ((wl_namespace = 1) AND (' .
|
|
"(((wl_title = 'AnotherDbKey') AND (" .
|
|
"(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
|
|
'))))' .
|
|
') OR ' .
|
|
'(makeWhereFrom2d return value)';
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
[ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
|
|
[ $expectedCond ],
|
|
$this->isType( 'string' ),
|
|
[
|
|
'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
|
|
]
|
|
)
|
|
->willReturn( $dbResult );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false
|
|
] );
|
|
|
|
$expected = [
|
|
0 => [
|
|
'SomeDbKey' => 100, 'OtherDbKey' => 300,
|
|
'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200
|
|
],
|
|
1 => [ 'AnotherDbKey' => 500 ],
|
|
];
|
|
$this->assertEquals(
|
|
$expected,
|
|
$store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactoryAndIntWithDbUnsafeVersion
|
|
*/
|
|
public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers, $testPageFactory ) {
|
|
$titleValuesWithThresholds = [
|
|
[ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ],
|
|
[ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ],
|
|
[ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ],
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->method( 'makeList' )
|
|
->willReturn( 'makeList return value' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
[ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
|
|
[ 'makeList return value' ],
|
|
$this->isType( 'string' ),
|
|
[
|
|
'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
|
|
'HAVING' => [ 'COUNT(*) >= 50' ],
|
|
]
|
|
)
|
|
->willReturn( [] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false,
|
|
] );
|
|
|
|
$expected = [
|
|
0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
|
|
1 => [ 'AnotherDbKey' => 0 ],
|
|
];
|
|
$this->assertEquals(
|
|
$expected,
|
|
$store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers )
|
|
);
|
|
}
|
|
|
|
public function testCountUnreadNotifications() {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectRowCount' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'1',
|
|
[
|
|
"wl_notificationtimestamp IS NOT NULL",
|
|
'wl_user' => 1,
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( '9' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideIntWithDbUnsafeVersion
|
|
*/
|
|
public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectRowCount' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'1',
|
|
[
|
|
"wl_notificationtimestamp IS NOT NULL",
|
|
'wl_user' => 1,
|
|
],
|
|
$this->isType( 'string' ),
|
|
[ 'LIMIT' => 50 ]
|
|
)
|
|
->willReturn( 50 );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertSame(
|
|
true,
|
|
$store->countUnreadNotifications( $user, $limit )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideIntWithDbUnsafeVersion
|
|
*/
|
|
public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectRowCount' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'1',
|
|
[
|
|
"wl_notificationtimestamp IS NOT NULL",
|
|
'wl_user' => 1,
|
|
],
|
|
$this->isType( 'string' ),
|
|
[ 'LIMIT' => 50 ]
|
|
)
|
|
->willReturn( 9 );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
9,
|
|
$store->countUnreadNotifications( $user, $limit )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testDuplicateEntry_nothingToDuplicate( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Old_Title',
|
|
],
|
|
'WatchedItemStore::fetchWatchedItemsForPage',
|
|
[ 'FOR UPDATE' ],
|
|
[ 'watchlist_expiry' => [ 'LEFT JOIN', [ 'wl_id = we_item' ] ] ]
|
|
)
|
|
->willReturn( new FakeResultWrapper( [] ) );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
|
|
|
|
$store->duplicateEntry(
|
|
$testPageFactory( 100, 0, 'Old_Title' ),
|
|
$testPageFactory( 101, 0, 'New_Title' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testDuplicateEntry_somethingToDuplicate( $testPageFactory ) {
|
|
$fakeRows = [
|
|
(object)[
|
|
'wl_user' => '1',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
],
|
|
(object)[
|
|
'wl_user' => '2',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb
|
|
->method( 'select' )
|
|
->withConsecutive(
|
|
[
|
|
[ 'watchlist' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp' ],
|
|
[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Old_Title',
|
|
]
|
|
],
|
|
[
|
|
'watchlist',
|
|
[ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
|
|
[
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'New_Title',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
],
|
|
[
|
|
'wl_user' => 2,
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'New_Title',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
],
|
|
$this->isType( 'string' )
|
|
]
|
|
)
|
|
->willReturnOnConsecutiveCalls(
|
|
new FakeResultWrapper( $fakeRows ),
|
|
$this->createMock( IResultWrapper::class )
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false,
|
|
] );
|
|
|
|
$store->duplicateEntry(
|
|
$testPageFactory( 100, 0, 'Old_Title' ),
|
|
$testPageFactory( 101, 0, 'New_Title' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testDuplicateAllAssociatedEntries_nothingToDuplicate( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb
|
|
->method( 'select' )
|
|
->withConsecutive(
|
|
[
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Old_Title',
|
|
]
|
|
],
|
|
[
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_namespace' => 1,
|
|
'wl_title' => 'Old_Title',
|
|
]
|
|
]
|
|
)
|
|
->willReturn( new FakeResultWrapper( [] ) );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$store->duplicateAllAssociatedEntries(
|
|
$testPageFactory( 100, 0, 'Old_Title' ),
|
|
$testPageFactory( 101, 0, 'New_Title' )
|
|
);
|
|
}
|
|
|
|
public function provideLinkTargetPairs() {
|
|
foreach ( $this->provideTestPageFactory() as $testPageFactoryArray ) {
|
|
$testPageFactory = $testPageFactoryArray[0];
|
|
yield [ $testPageFactory( 100, 0, 'Old_Title' ), $testPageFactory( 101, 0, 'New_Title' ) ];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param LinkTarget|PageIdentity $oldTarget
|
|
* @param LinkTarget|PageIdentity $newTarget
|
|
* @dataProvider provideLinkTargetPairs
|
|
*/
|
|
public function testDuplicateAllAssociatedEntries_somethingToDuplicate(
|
|
$oldTarget,
|
|
$newTarget
|
|
) {
|
|
$fakeRows = [
|
|
(object)[
|
|
'wl_user' => '1',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => null,
|
|
],
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb
|
|
->method( 'select' )
|
|
->withConsecutive(
|
|
[
|
|
[ 'watchlist' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp' ],
|
|
[
|
|
'wl_namespace' => $oldTarget->getNamespace(),
|
|
'wl_title' => $oldTarget->getDBkey(),
|
|
]
|
|
],
|
|
[
|
|
[ 'watchlist' ],
|
|
[ 'wl_user', 'wl_notificationtimestamp' ],
|
|
[
|
|
'wl_namespace' => $oldTarget->getNamespace() + 1,
|
|
'wl_title' => $oldTarget->getDBkey(),
|
|
]
|
|
]
|
|
)
|
|
->willReturn( new FakeResultWrapper( $fakeRows ) );
|
|
$mockDb
|
|
->method( 'replace' )
|
|
->withConsecutive(
|
|
[
|
|
'watchlist',
|
|
[ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
|
|
[
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => $newTarget->getNamespace(),
|
|
'wl_title' => $newTarget->getDBkey(),
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
],
|
|
],
|
|
$this->isType( 'string' )
|
|
],
|
|
[
|
|
'watchlist',
|
|
[ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
|
|
[
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => $newTarget->getNamespace() + 1,
|
|
'wl_title' => $newTarget->getDBkey(),
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
],
|
|
],
|
|
$this->isType( 'string' )
|
|
]
|
|
);
|
|
$mockDb
|
|
->method( 'newSelectQueryBuilder' )
|
|
->willReturn( new SelectQueryBuilder( $mockDb ) );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
'expiryEnabled' => false,
|
|
] );
|
|
|
|
$store->duplicateAllAssociatedEntries(
|
|
$oldTarget,
|
|
$newTarget
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testAddWatch_nonAnonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'insert' )
|
|
->with(
|
|
'watchlist',
|
|
[
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Some_Page',
|
|
'wl_notificationtimestamp' => null,
|
|
]
|
|
]
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:Some_Page:1' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$store->addWatch(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'Some_Page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testAddWatch_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'insert' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )
|
|
->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$store->addWatch(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'Some_Page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testAddWatchBatchForUser_readOnlyDBReturnsFalse( $testPageFactory ) {
|
|
$store = $this->newWatchedItemStore(
|
|
[ 'readOnlyMode' => $this->getDummyReadOnlyMode( true ) ]
|
|
);
|
|
|
|
$this->assertFalse(
|
|
$store->addWatchBatchForUser(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
[ $testPageFactory( 100, 0, 'Some_Page' ), $testPageFactory( 101, 1, 'Some_Page' ) ]
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testAddWatchBatchForUser_nonAnonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'insert' )
|
|
->with(
|
|
'watchlist',
|
|
[
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Some_Page',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
[
|
|
'wl_user' => 1,
|
|
'wl_namespace' => 1,
|
|
'wl_title' => 'Some_Page',
|
|
'wl_notificationtimestamp' => null,
|
|
]
|
|
]
|
|
);
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'affectedRows' )
|
|
->willReturn( 2 );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->exactly( 2 ) )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
[ '0:Some_Page:1' ],
|
|
[ '1:Some_Page:1' ]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$mockUser = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$this->assertTrue(
|
|
$store->addWatchBatchForUser(
|
|
$mockUser,
|
|
[ $testPageFactory( 100, 0, 'Some_Page' ), $testPageFactory( 101, 1, 'Some_Page' ) ]
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testAddWatchBatchForUser_anonymousUsersAreSkipped( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'insert' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )
|
|
->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->addWatchBatchForUser(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
[ $testPageFactory( 100, 0, 'Other_Page' ) ]
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'insert' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )
|
|
->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertTrue(
|
|
$store->addWatchBatchForUser( $user, [] )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testLoadWatchedItem_existingItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => '20300101000000'
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with(
|
|
'0:SomeDbKey:1'
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$watchedItem = $store->loadWatchedItem(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
);
|
|
$this->assertInstanceOf( WatchedItem::class, $watchedItem );
|
|
$this->assertSame( 1, $watchedItem->getUserIdentity()->getId() );
|
|
$this->assertEquals( 'SomeDbKey', $watchedItem->getTarget()->getDBkey() );
|
|
$this->assertSame( '20300101000000', $watchedItem->getExpiry() );
|
|
$this->assertSame( 0, $watchedItem->getTarget()->getNamespace() );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testLoadWatchedItem_noItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->willReturn( [] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->loadWatchedItem(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testLoadWatchedItem_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'select' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->loadWatchedItem(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testRemoveWatch_existingItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectFieldValues' )
|
|
->willReturn( [ 1, 2 ] );
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
[
|
|
'watchlist',
|
|
[ 'wl_id' => [ 1, 2 ] ]
|
|
],
|
|
[
|
|
'watchlist_expiry',
|
|
[ 'we_item' => [ 1, 2 ] ]
|
|
]
|
|
);
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'affectedRows' )
|
|
->willReturn( 2 );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
[ '0:SomeDbKey:1' ],
|
|
[ '1:SomeDbKey:1' ]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertTrue(
|
|
$store->removeWatch(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testRemoveWatch_noItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectFieldValues' )
|
|
->willReturn( [] );
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'delete' );
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'affectedRows' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
[ '0:SomeDbKey:1' ],
|
|
[ '1:SomeDbKey:1' ]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->removeWatch(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testRemoveWatch_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'delete' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )
|
|
->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->removeWatch(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetWatchedItem_existingItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => '20300101000000'
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with(
|
|
'0:SomeDbKey:1'
|
|
)
|
|
->willReturn( null );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with(
|
|
'0:SomeDbKey:1'
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$watchedItem = $store->getWatchedItem(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
);
|
|
$this->assertInstanceOf( WatchedItem::class, $watchedItem );
|
|
$this->assertSame( 1, $watchedItem->getUserIdentity()->getId() );
|
|
$this->assertEquals( 'SomeDbKey', $watchedItem->getTarget()->getDBkey() );
|
|
$this->assertSame( '20300101000000', $watchedItem->getExpiry() );
|
|
$this->assertSame( 0, $watchedItem->getTarget()->getNamespace() );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetWatchedItem_cachedItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockUser = new UserIdentityValue( 1, 'MockUser' );
|
|
$linkTarget = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
$cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with(
|
|
'0:SomeDbKey:1'
|
|
)
|
|
->willReturn( $cachedItem );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
$cachedItem,
|
|
$store->getWatchedItem(
|
|
$mockUser,
|
|
$linkTarget
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetWatchedItem_noItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with( '0:SomeDbKey:1' )
|
|
->willReturn( false );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->getWatchedItem(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetWatchedItem_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->getWatchedItem(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testGetWatchedItemsForUser() {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Foo1',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => '20300101000000'
|
|
],
|
|
(object)[
|
|
'wl_namespace' => 1,
|
|
'wl_title' => 'Foo2',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$watchedItems = $store->getWatchedItemsForUser( $user );
|
|
|
|
$this->assertIsArray( $watchedItems );
|
|
$this->assertCount( 2, $watchedItems );
|
|
foreach ( $watchedItems as $watchedItem ) {
|
|
$this->assertInstanceOf( WatchedItem::class, $watchedItem );
|
|
}
|
|
$this->assertEquals(
|
|
new WatchedItem(
|
|
$user,
|
|
new TitleValue( 0, 'Foo1' ),
|
|
'20151212010101',
|
|
'20300101000000'
|
|
),
|
|
$watchedItems[0]
|
|
);
|
|
$this->assertEquals(
|
|
new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
|
|
$watchedItems[1]
|
|
);
|
|
}
|
|
|
|
public static function provideDbTypes() {
|
|
return [
|
|
[ false, DB_REPLICA ],
|
|
[ true, DB_PRIMARY ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideDbTypes
|
|
*/
|
|
public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockCache = $this->getMockCache();
|
|
$mockLoadBalancer = $this->getMockLBFactory( $mockDb, $dbType );
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ],
|
|
$this->isType( 'string' ),
|
|
[ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
|
|
)
|
|
->willReturn( [] );
|
|
|
|
$store = $this->newWatchedItemStore(
|
|
[ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] );
|
|
|
|
$watchedItems = $store->getWatchedItemsForUser(
|
|
$user,
|
|
[ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ]
|
|
);
|
|
$this->assertEquals( [], $watchedItems );
|
|
}
|
|
|
|
public function testGetWatchedItemsForUser_sortByExpiry() {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[
|
|
'wl_namespace',
|
|
'wl_title',
|
|
'wl_notificationtimestamp',
|
|
'we_expiry',
|
|
'wl_has_expiry' => null
|
|
],
|
|
[ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Foo1',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => '20300101000000'
|
|
],
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'Foo2',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
'we_expiry' => '20300701000000'
|
|
],
|
|
(object)[
|
|
'wl_namespace' => 1,
|
|
'wl_title' => 'Foo3',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$watchedItems = $store->getWatchedItemsForUser(
|
|
$user,
|
|
[ 'sortByExpiry' => true, 'sort' => WatchedItemStore::SORT_ASC ]
|
|
);
|
|
|
|
$this->assertIsArray( $watchedItems );
|
|
$this->assertCount( 3, $watchedItems );
|
|
foreach ( $watchedItems as $watchedItem ) {
|
|
$this->assertInstanceOf( WatchedItem::class, $watchedItem );
|
|
}
|
|
$this->assertEquals(
|
|
new WatchedItem(
|
|
$user,
|
|
new TitleValue( 0, 'Foo1' ),
|
|
'20151212010101',
|
|
'20300101000000'
|
|
),
|
|
$watchedItems[0]
|
|
);
|
|
$this->assertEquals(
|
|
new WatchedItem( $user, new TitleValue( 1, 'Foo3' ), null ),
|
|
$watchedItems[2]
|
|
);
|
|
}
|
|
|
|
public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
|
|
$store = $this->newWatchedItemStore();
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$store->getWatchedItemsForUser(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
[ 'sort' => 'foo' ]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testIsWatchedItem_existingItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with( '0:SomeDbKey:1' )
|
|
->willReturn( false );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with(
|
|
'0:SomeDbKey:1'
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertTrue(
|
|
$store->isWatched(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testIsWatchedItem_noItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with( '0:SomeDbKey:1' )
|
|
->willReturn( false );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->isWatched(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testIsWatchedItem_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->isWatched(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetNotificationTimestampsBatch( $testPageFactory ) {
|
|
$targets = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$dbResult = [
|
|
(object)[
|
|
'wl_namespace' => '0',
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
],
|
|
(object)[
|
|
'wl_namespace' => '1',
|
|
'wl_title' => 'AnotherDbKey',
|
|
'wl_notificationtimestamp' => null,
|
|
],
|
|
];
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
|
|
[
|
|
'makeWhereFrom2d return value',
|
|
'wl_user' => 1
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( $dbResult );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->exactly( 2 ) )
|
|
->method( 'get' )
|
|
->withConsecutive(
|
|
[ '0:SomeDbKey:1' ],
|
|
[ '1:AnotherDbKey:1' ]
|
|
)
|
|
->willReturn( null );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
[
|
|
0 => [ 'SomeDbKey' => '20151212010101', ],
|
|
1 => [ 'AnotherDbKey' => null, ],
|
|
],
|
|
$store->getNotificationTimestampsBatch(
|
|
new UserIdentityValue( 1, 'MockUser' ), $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetNotificationTimestampsBatch_notWatchedTarget( $testPageFactory ) {
|
|
$targets = [
|
|
$testPageFactory( 100, 0, 'OtherDbKey' ),
|
|
];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ [ 'OtherDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
|
|
[
|
|
'makeWhereFrom2d return value',
|
|
'wl_user' => 1
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( (object)[] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with( '0:OtherDbKey:1' )
|
|
->willReturn( null );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
[
|
|
0 => [ 'OtherDbKey' => false, ],
|
|
],
|
|
$store->getNotificationTimestampsBatch(
|
|
new UserIdentityValue( 1, 'MockUser' ), $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetNotificationTimestampsBatch_cachedItem( $testPageFactory ) {
|
|
$targets = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeWhereFrom2d' )
|
|
->with(
|
|
[ 1 => [ 'AnotherDbKey' => 1 ] ],
|
|
$this->isType( 'string' ),
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( 'makeWhereFrom2d return value' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
|
|
[
|
|
'makeWhereFrom2d return value',
|
|
'wl_user' => 1
|
|
],
|
|
$this->isType( 'string' )
|
|
)
|
|
->willReturn( [
|
|
(object)[ 'wl_namespace' => '1', 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache
|
|
->method( 'get' )
|
|
->withConsecutive(
|
|
[ '0:SomeDbKey:1' ],
|
|
[ '1:AnotherDbKey:1' ]
|
|
)
|
|
->willReturnOnConsecutiveCalls( $cachedItem, null );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
[
|
|
0 => [ 'SomeDbKey' => '20151212010101', ],
|
|
1 => [ 'AnotherDbKey' => null, ],
|
|
],
|
|
$store->getNotificationTimestampsBatch( $user, $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetNotificationTimestampsBatch_allItemsCached( $testPageFactory ) {
|
|
$targets = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$cachedItems = [
|
|
new WatchedItem( $user, $targets[0], '20151212010101' ),
|
|
new WatchedItem( $user, $targets[1], null ),
|
|
];
|
|
$mockDb = $this->createNoOpMock( DBConnRef::class );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache
|
|
->method( 'get' )
|
|
->withConsecutive(
|
|
[ '0:SomeDbKey:1' ],
|
|
[ '1:AnotherDbKey:1' ]
|
|
)
|
|
->willReturnOnConsecutiveCalls( $cachedItems[0], $cachedItems[1] );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
[
|
|
0 => [ 'SomeDbKey' => '20151212010101', ],
|
|
1 => [ 'AnotherDbKey' => null, ],
|
|
],
|
|
$store->getNotificationTimestampsBatch( $user, $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testGetNotificationTimestampsBatch_anonymousUser( $testPageFactory ) {
|
|
$targets = [
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
$testPageFactory( 101, 1, 'AnotherDbKey' ),
|
|
];
|
|
|
|
$mockDb = $this->createNoOpMock( DBConnRef::class );
|
|
|
|
$mockCache = $this->createNoOpMock( HashBagOStuff::class );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertEquals(
|
|
[
|
|
0 => [ 'SomeDbKey' => false, ],
|
|
1 => [ 'AnotherDbKey' => false, ],
|
|
],
|
|
$store->getNotificationTimestampsBatch(
|
|
new UserIdentityValue( 0, 'AnonUser' ), $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_anonymousUser( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$this->assertFalse(
|
|
$store->resetNotificationTimestamp(
|
|
new UserIdentityValue( 0, 'AnonUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' )
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_noItem( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'cache' => $mockCache,
|
|
] );
|
|
|
|
$this->assertFalse(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_item( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with(
|
|
'0:SomeDbKey:1',
|
|
$this->isInstanceOf( WatchedItem::class )
|
|
);
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
$mockQueueGroup->expects( $this->once() )
|
|
->method( 'lazyPush' )
|
|
->willReturnCallback( static function ( ActivityUpdateJob $job ) {
|
|
// don't run
|
|
} );
|
|
|
|
// We don't care if these methods actually do anything here
|
|
$mockRevisionLookup = $this->getMockRevisionLookup( [
|
|
'getRevisionByTitle' => static function () {
|
|
return null;
|
|
},
|
|
'getTimestampFromId' => static function () {
|
|
return '00000000000000';
|
|
},
|
|
] );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_noItemForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
// We don't care if these methods actually do anything here
|
|
$mockRevisionLookup = $this->getMockRevisionLookup( [
|
|
'getRevisionByTitle' => static function () {
|
|
return null;
|
|
},
|
|
'getTimestampFromId' => static function () {
|
|
return '00000000000000';
|
|
},
|
|
] );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback( static function ( ActivityUpdateJob $job ) {
|
|
// don't run
|
|
} );
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'force'
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param ActivityUpdateJob $job
|
|
* @param LinkTarget|PageIdentity $expectedTitle
|
|
* @param string $expectedUserId
|
|
* @param callable $notificationTimestampCondition
|
|
*/
|
|
private function verifyCallbackJob(
|
|
ActivityUpdateJob $job,
|
|
$expectedTitle,
|
|
$expectedUserId,
|
|
callable $notificationTimestampCondition
|
|
) {
|
|
$this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() );
|
|
$this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() );
|
|
|
|
$jobParams = $job->getParams();
|
|
$this->assertArrayHasKey( 'type', $jobParams );
|
|
$this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] );
|
|
$this->assertArrayHasKey( 'userid', $jobParams );
|
|
$this->assertEquals( $expectedUserId, $jobParams['userid'] );
|
|
$this->assertArrayHasKey( 'notifTime', $jobParams );
|
|
$this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeTitle' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'selectRow' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeTitle:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
$mockRevisionRecord = $this->createNoOpMock( RevisionRecord::class );
|
|
|
|
$mockRevisionLookup = $this->getMockRevisionLookup( [
|
|
'getTimestampFromId' => static function () {
|
|
return '00000000000000';
|
|
},
|
|
'getRevisionById' => function ( $id, $flags ) use ( $oldid, $mockRevisionRecord ) {
|
|
$this->assertSame( $oldid, $id );
|
|
$this->assertSame( 0, $flags );
|
|
return $mockRevisionRecord;
|
|
},
|
|
'getNextRevision' =>
|
|
function ( $oldRev ) use ( $mockRevisionRecord ) {
|
|
$this->assertSame( $mockRevisionRecord, $oldRev );
|
|
return false;
|
|
},
|
|
], [
|
|
'getNextRevision' => 1,
|
|
] );
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback(
|
|
function ( ActivityUpdateJob $job ) use ( $title, $user ) {
|
|
$this->verifyCallbackJob(
|
|
$job,
|
|
$title,
|
|
$user->getId(),
|
|
static function ( $time ) {
|
|
return $time === null;
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'force',
|
|
$oldid
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101',
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
$mockRevisionLookup = $this->getMockRevisionLookup(
|
|
[
|
|
'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
|
|
$this->assertSame( $oldid, $oldidParam );
|
|
},
|
|
'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
|
|
$this->assertSame( $oldid, $id );
|
|
return $mockRevision;
|
|
},
|
|
'getNextRevision' =>
|
|
function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
|
|
$this->assertSame( $mockRevision, $rev );
|
|
return $mockNextRevision;
|
|
},
|
|
],
|
|
[
|
|
'getTimestampFromId' => 2,
|
|
'getRevisionById' => 1,
|
|
'getNextRevision' => 1,
|
|
]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback(
|
|
function ( ActivityUpdateJob $job ) use ( $title, $user ) {
|
|
$this->verifyCallbackJob(
|
|
$job,
|
|
$title,
|
|
$user->getId(),
|
|
static function ( $time ) {
|
|
return $time !== null && $time > '20151212010101';
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'force',
|
|
$oldid
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_notWatchedPageForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( false );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
$mockRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
|
|
$mockRevisionLookup = $this->getMockRevisionLookup(
|
|
[
|
|
'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
|
|
$this->assertSame( $oldid, $oldidParam );
|
|
},
|
|
'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
|
|
$this->assertSame( $oldid, $id );
|
|
return $mockRevision;
|
|
},
|
|
'getNextRevision' =>
|
|
function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
|
|
$this->assertSame( $mockRevision, $rev );
|
|
return $mockNextRevision;
|
|
},
|
|
],
|
|
[
|
|
'getTimestampFromId' => 1,
|
|
'getRevisionById' => 1,
|
|
'getNextRevision' => 1,
|
|
]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback(
|
|
function ( ActivityUpdateJob $job ) use ( $title, $user ) {
|
|
$this->verifyCallbackJob(
|
|
$job,
|
|
$title,
|
|
$user->getId(),
|
|
static function ( $time ) {
|
|
return $time === null;
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'force',
|
|
$oldid
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_futureNotificationTimestampForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000'
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '30151212010101',
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
$mockRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
|
|
$mockRevisionLookup = $this->getMockRevisionLookup(
|
|
[
|
|
'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
|
|
$this->assertEquals( $oldid, $oldidParam );
|
|
},
|
|
'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
|
|
$this->assertSame( $oldid, $id );
|
|
return $mockRevision;
|
|
},
|
|
'getNextRevision' =>
|
|
function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
|
|
$this->assertSame( $mockRevision, $rev );
|
|
return $mockNextRevision;
|
|
},
|
|
],
|
|
[
|
|
'getTimestampFromId' => 2,
|
|
'getRevisionById' => 1,
|
|
'getNextRevision' => 1,
|
|
]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback(
|
|
function ( ActivityUpdateJob $job ) use ( $title, $user ) {
|
|
$this->verifyCallbackJob(
|
|
$job,
|
|
$title,
|
|
$user->getId(),
|
|
static function ( $time ) {
|
|
return $time === '30151212010101';
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'force',
|
|
$oldid
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_futureNotificationTimestampNotForced( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'";
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'makeList' )
|
|
->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ],
|
|
[
|
|
'wl_user' => 1,
|
|
$makeListSql,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000',
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '30151212010101',
|
|
]
|
|
] );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )->method( 'get' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$mockQueueGroup = $this->getMockJobQueueGroup( false );
|
|
|
|
$mockRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
|
|
$mockRevisionLookup = $this->getMockRevisionLookup(
|
|
[
|
|
'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
|
|
$this->assertEquals( $oldid, $oldidParam );
|
|
},
|
|
'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
|
|
$this->assertSame( $oldid, $id );
|
|
return $mockRevision;
|
|
},
|
|
'getNextRevision' =>
|
|
function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
|
|
$this->assertSame( $mockRevision, $rev );
|
|
return $mockNextRevision;
|
|
},
|
|
],
|
|
[
|
|
'getTimestampFromId' => 2,
|
|
'getRevisionById' => 1,
|
|
'getNextRevision' => 1,
|
|
]
|
|
);
|
|
|
|
$store = $this->newWatchedItemStore( [
|
|
'db' => $mockDb,
|
|
'queueGroup' => $mockQueueGroup,
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
] );
|
|
|
|
$mockQueueGroup->method( 'lazyPush' )
|
|
->willReturnCallback(
|
|
function ( ActivityUpdateJob $job ) use ( $title, $user ) {
|
|
$this->verifyCallbackJob(
|
|
$job,
|
|
$title,
|
|
$user->getId(),
|
|
static function ( $time ) {
|
|
return $time === false;
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
$this->assertTrue(
|
|
$store->resetNotificationTimestamp(
|
|
$user,
|
|
$title,
|
|
'',
|
|
$oldid
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testSetNotificationTimestampsForUser_anonUser() {
|
|
$store = $this->newWatchedItemStore();
|
|
$this->assertFalse( $store->setNotificationTimestampsForUser(
|
|
new UserIdentityValue( 0, 'AnonUser' ), '' ) );
|
|
}
|
|
|
|
public function testSetNotificationTimestampsForUser_allRows() {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$timestamp = '20100101010101';
|
|
|
|
$store = $this->newWatchedItemStore();
|
|
|
|
// Note: This does not actually assert the job is correct
|
|
$callableCallCounter = 0;
|
|
$mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
|
|
$callableCallCounter++;
|
|
$this->assertIsCallable( $callable );
|
|
};
|
|
$scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
|
|
|
|
$this->assertTrue(
|
|
$store->setNotificationTimestampsForUser( $user, $timestamp )
|
|
);
|
|
$this->assertSame( 1, $callableCallCounter );
|
|
}
|
|
|
|
public function testSetNotificationTimestampsForUser_nullTimestamp() {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$timestamp = null;
|
|
|
|
$store = $this->newWatchedItemStore();
|
|
|
|
// Note: This does not actually assert the job is correct
|
|
$callableCallCounter = 0;
|
|
$mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
|
|
$callableCallCounter++;
|
|
$this->assertIsCallable( $callable );
|
|
};
|
|
$scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
|
|
|
|
$this->assertTrue(
|
|
$store->setNotificationTimestampsForUser( $user, $timestamp )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testSetNotificationTimestampsForUser_specificTargets( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$timestamp = '20100101010101';
|
|
$targets = [ $testPageFactory( 100, 0, 'Foo' ), $testPageFactory( 101, 0, 'Bar' ) ];
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'selectFieldValues' )
|
|
->with(
|
|
[ 'watchlist' ],
|
|
'wl_id',
|
|
[ 'wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => [ 'Foo', 'Bar' ] ]
|
|
)
|
|
->willReturn( [ '2', '3' ] );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'update' )
|
|
->with(
|
|
'watchlist',
|
|
[ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
|
|
[ 'wl_id' => [ 2, 3 ] ]
|
|
)
|
|
->willReturn( true );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'timestamp' )
|
|
->willReturnCallback( static function ( $value ) {
|
|
return 'TS' . $value . 'TS';
|
|
} );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'affectedRows' )
|
|
->willReturn( 2 );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
|
|
|
|
$this->assertTrue(
|
|
$store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testUpdateNotificationTimestamp_watchersExist( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_id', 'wl_user' ],
|
|
[
|
|
'wl_user != 1',
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => null,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000',
|
|
]
|
|
)
|
|
->willReturn( [
|
|
(object)[
|
|
'wl_id' => '4',
|
|
'wl_user' => '2',
|
|
],
|
|
(object)[
|
|
'wl_id' => '5',
|
|
'wl_user' => '3',
|
|
],
|
|
] );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'update' )
|
|
->with(
|
|
'watchlist',
|
|
[ 'wl_notificationtimestamp' => null ],
|
|
[
|
|
'wl_id' => [ 4, 5 ],
|
|
]
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
// updateNotificationTimestamp calls DeferredUpdates::addCallableUpdate
|
|
// in normal operation, but we want to test that update actually running, so
|
|
// override it
|
|
$mockCallback = function ( $callable, $stage, $dbw ) use ( $mockDb ) {
|
|
$this->assertIsCallable( $callable );
|
|
$this->assertSame( DeferredUpdates::POSTSEND, $stage );
|
|
$this->assertSame( $mockDb, $dbw );
|
|
( $callable )();
|
|
};
|
|
$scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
|
|
|
|
$this->assertEquals(
|
|
[ 2, 3 ],
|
|
$store->updateNotificationTimestamp(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
'20151212010101'
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testUpdateNotificationTimestamp_noWatchers( $testPageFactory ) {
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'select' )
|
|
->with(
|
|
[ 'watchlist', 'watchlist_expiry' => 'watchlist_expiry' ],
|
|
[ 'wl_id', 'wl_user' ],
|
|
[
|
|
'wl_user != 1',
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => null,
|
|
'we_expiry IS NULL OR we_expiry > 20200101000000',
|
|
],
|
|
'WatchedItemStore::updateNotificationTimestamp',
|
|
[],
|
|
[ 'watchlist_expiry' => [ 'LEFT JOIN', 'wl_id = we_item' ] ]
|
|
)
|
|
->willReturn( [] );
|
|
$mockDb->expects( $this->never() )
|
|
->method( 'update' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->never() )->method( 'set' );
|
|
$mockCache->expects( $this->never() )->method( 'get' );
|
|
$mockCache->expects( $this->never() )->method( 'delete' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
$watchers = $store->updateNotificationTimestamp(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$testPageFactory( 100, 0, 'SomeDbKey' ),
|
|
'20151212010101'
|
|
);
|
|
$this->assertSame( [], $watchers );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testUpdateNotificationTimestamp_clearsCachedItems( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$titleValue = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
|
|
$mockDb = $this->getMockDb();
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'select' )
|
|
->willReturnOnConsecutiveCalls( [
|
|
(object)[
|
|
'wl_namespace' => 0,
|
|
'wl_title' => 'SomeDbKey',
|
|
'wl_notificationtimestamp' => '20151212010101'
|
|
]
|
|
],
|
|
[
|
|
(object)[
|
|
'wl_id' => '4',
|
|
'wl_user' => '2',
|
|
],
|
|
(object)[
|
|
'wl_id' => '5',
|
|
'wl_user' => '3',
|
|
],
|
|
] );
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'update' );
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'set' )
|
|
->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'get' )
|
|
->with( '0:SomeDbKey:1' );
|
|
$mockCache->expects( $this->once() )
|
|
->method( 'delete' )
|
|
->with( '0:SomeDbKey:1' );
|
|
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
|
|
// This will add the item to the cache
|
|
$store->getWatchedItem( $user, $titleValue );
|
|
|
|
// updateNotificationTimestamp calls DeferredUpdates::addCallableUpdate
|
|
// in normal operation, but we want to test that update actually running, so
|
|
// override it
|
|
$mockCallback = function ( $callable, $stage, $dbw ) use ( $mockDb ) {
|
|
$this->assertIsCallable( $callable );
|
|
$this->assertSame( DeferredUpdates::POSTSEND, $stage );
|
|
$this->assertSame( $mockDb, $dbw );
|
|
( $callable )();
|
|
};
|
|
$scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
|
|
|
|
$store->updateNotificationTimestamp(
|
|
new UserIdentityValue( 1, 'MockUser' ),
|
|
$titleValue,
|
|
'20151212010101'
|
|
);
|
|
}
|
|
|
|
public function testRemoveExpired() {
|
|
$mockDb = $this->getMockDb();
|
|
|
|
// addQuotes is used for the expiry value.
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'addQuotes' )
|
|
->willReturn( '20200101000000' );
|
|
|
|
// Select watchlist IDs.
|
|
$mockDb->expects( $this->exactly( 2 ) )
|
|
->method( 'selectFieldValues' )
|
|
->withConsecutive(
|
|
// Select expired items.
|
|
[
|
|
[ 'watchlist_expiry' ],
|
|
'we_item',
|
|
[ 'we_expiry <= 20200101000000' ],
|
|
'WatchedItemStore::removeExpired',
|
|
[ 'LIMIT' => 2 ]
|
|
],
|
|
// Select orphaned items.
|
|
[
|
|
[ 'watchlist_expiry', 'watchlist' => 'watchlist' ],
|
|
'we_item',
|
|
[ 'wl_id' => null, 'we_expiry' => null ],
|
|
'WatchedItemStore::removeExpired',
|
|
[],
|
|
[ 'watchlist' => [ 'LEFT JOIN', 'wl_id = we_item' ] ]
|
|
]
|
|
)
|
|
->willReturnOnConsecutiveCalls(
|
|
[ 1, 2 ],
|
|
[ 3 ]
|
|
);
|
|
|
|
// Return whatever is passed to makeList, to be tested below.
|
|
$mockDb->expects( $this->once() )
|
|
->method( 'makeList' )
|
|
->willReturnArgument( 0 );
|
|
|
|
// Delete from watchlist and watchlist_expiry.
|
|
$mockDb->expects( $this->exactly( 3 ) )
|
|
->method( 'delete' )
|
|
->withConsecutive(
|
|
// Delete expired items from watchlist
|
|
[
|
|
'watchlist',
|
|
[ 'wl_id' => [ 1, 2 ] ],
|
|
'WatchedItemStore::removeExpired'
|
|
],
|
|
// Delete expired items from watchlist_expiry
|
|
[
|
|
'watchlist_expiry',
|
|
[ 'we_item' => [ 1, 2 ] ],
|
|
'WatchedItemStore::removeExpired'
|
|
],
|
|
// Delete orphaned items
|
|
[
|
|
'watchlist_expiry',
|
|
[ 'we_item' => [ 3 ] ],
|
|
'WatchedItemStore::removeExpired'
|
|
]
|
|
);
|
|
|
|
$mockCache = $this->getMockCache();
|
|
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
|
|
$store->removeExpired( 2, true );
|
|
}
|
|
|
|
public static function provideGetLatestNotificationTimestamp() {
|
|
$emptyMap = new MapCacheLRU( 300 );
|
|
$oldMap = new MapCacheLRU( 300 );
|
|
$oldMap->set( '0:Title', '20090101000000' );
|
|
$newMap = new MapCacheLRU( 300 );
|
|
$newMap->set( '0:Title', '20110101000000' );
|
|
$wrongKeyMap = new MapCacheLRU( 300 );
|
|
$wrongKeyMap->set( '0:Wrong', '20110101000000' );
|
|
// Arrays are used for stash values after T282105. We test forwards and
|
|
// backwards compatibility. The MapCacheLRU cases can be removed after
|
|
// deployment of T282105 has finished.
|
|
return [
|
|
'empty cache' => [
|
|
null,
|
|
true
|
|
],
|
|
'empty MapCacheLRU' => [
|
|
$emptyMap,
|
|
true
|
|
],
|
|
'empty array' => [
|
|
$emptyMap->toArray(),
|
|
true
|
|
],
|
|
'old MapCacheLRU' => [
|
|
$oldMap,
|
|
true,
|
|
],
|
|
'old array' => [
|
|
$oldMap->toArray(),
|
|
true
|
|
],
|
|
'new MapCacheLRU' => [
|
|
$newMap,
|
|
false
|
|
],
|
|
'new array' => [
|
|
$newMap->toArray(),
|
|
false
|
|
],
|
|
'wrong key MapCacheLRU' => [
|
|
$wrongKeyMap,
|
|
true
|
|
],
|
|
'wrong key array' => [
|
|
$wrongKeyMap->toArray(),
|
|
true
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @dataProvider provideGetLatestNotificationTimestamp */
|
|
public function testGetLatestNotificationTimestamp( $cacheValue, $expectNonNull ) {
|
|
$user = new UserIdentityValue( 1, 'User' );
|
|
$title = new TitleValue( 0, 'Title' );
|
|
$stash = new HashBagOStuff;
|
|
$stash->set(
|
|
$stash->makeGlobalKey(
|
|
'watchlist-recent-updates',
|
|
'phpunitdb',
|
|
$user->getId()
|
|
),
|
|
$cacheValue
|
|
);
|
|
$store = $this->newWatchedItemStore( [ 'stash' => $stash ] );
|
|
$result = $store->getLatestNotificationTimestamp(
|
|
'20100101000000', $user, $title );
|
|
$this->assertSame( $expectNonNull, $result !== null );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideTestPageFactory
|
|
*/
|
|
public function testResetNotificationTimestamp_stashItemTypeCheck( $testPageFactory ) {
|
|
$user = new UserIdentityValue( 1, 'MockUser' );
|
|
$oldid = 22;
|
|
$title = $testPageFactory( 100, 0, 'SomeDbKey' );
|
|
$stash = new HashBagOStuff;
|
|
$mockCache = $this->getMockCache();
|
|
$mockRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
|
|
$mockRevisionLookup = $this->getMockRevisionLookup(
|
|
[
|
|
'getTimestampFromId' => static function () {
|
|
return '00000000000000';
|
|
},
|
|
'getRevisionByTitle' => static function () {
|
|
return null;
|
|
},
|
|
'getRevisionById' => static function ( $id ) use ( $mockRevision ) {
|
|
return $mockRevision;
|
|
},
|
|
'getNextRevision' => static function ( RevisionRecord $rev ) use ( $mockNextRevision ) {
|
|
return $mockNextRevision;
|
|
},
|
|
]
|
|
);
|
|
$store = $this->newWatchedItemStore( [
|
|
'cache' => $mockCache,
|
|
'revisionLookup' => $mockRevisionLookup,
|
|
'stash' => $stash,
|
|
'queueGroup' => $this->getMockJobQueueGroup( false ),
|
|
] );
|
|
$store->resetNotificationTimestamp( $user,
|
|
$title,
|
|
'force',
|
|
$oldid );
|
|
$this->assertIsArray( $stash->get( 'global:watchlist-recent-updates:phpunitdb:1' ) );
|
|
}
|
|
}
|