wiki.techinc.nl/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
Leszek Manicki c1b4b19de3 Refactor database-related code in ApiQueryWatchlistRaw
Database queries used to get user's watchlist items in API are quite
complex due to number of options oferred by the API.
Generating the query is moved to WatchedItemQueryService.
ApiQueryWatchlistRaw no longer contains database-related code.

Simple user watchlist item lookups should use WatchedItemStore.

ApiQueryWatchlistRaw tests have been introduced in
I9c07aa237607143985f0efe20ed0065d2bde27e4

Bug: T132566
Change-Id: I875a92074b52c00ac11db1fa05615abbf5262ab1
2016-07-06 12:57:39 +00:00

1312 lines
34 KiB
PHP

<?php
/**
* @covers WatchedItemQueryService
*/
class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
/**
* @return PHPUnit_Framework_MockObject_MockObject|DatabaseBase
*/
private function getMockDb() {
$mock = $this->getMockBuilder( DatabaseBase::class )
->disableOriginalConstructor()
->getMock();
$mock->expects( $this->any() )
->method( 'makeList' )
->with(
$this->isType( 'array' ),
$this->isType( 'int' )
)
->will( $this->returnCallback( function( $a, $conj ) {
$sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
return join( $sqlConj, array_map( function( $s ) {
return '(' . $s . ')';
}, $a
) );
} ) );
$mock->expects( $this->any() )
->method( 'addQuotes' )
->will( $this->returnCallback( function( $value ) {
return "'$value'";
} ) );
$mock->expects( $this->any() )
->method( 'timestamp' )
->will( $this->returnArgument( 0 ) );
$mock->expects( $this->any() )
->method( 'bitAnd' )
->willReturnCallback( function( $a, $b ) {
return "($a & $b)";
} );
return $mock;
}
/**
* @param $mockDb
* @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
*/
private function getMockLoadBalancer( $mockDb ) {
$mock = $this->getMockBuilder( LoadBalancer::class )
->disableOriginalConstructor()
->getMock();
$mock->expects( $this->any() )
->method( 'getConnection' )
->with( DB_SLAVE )
->will( $this->returnValue( $mockDb ) );
return $mock;
}
/**
* @param int $id
* @return PHPUnit_Framework_MockObject_MockObject|User
*/
private function getMockNonAnonUserWithId( $id ) {
$mock = $this->getMock( User::class );
$mock->expects( $this->any() )
->method( 'isAnon' )
->will( $this->returnValue( false ) );
$mock->expects( $this->any() )
->method( 'getId' )
->will( $this->returnValue( $id ) );
return $mock;
}
/**
* @param int $id
* @return PHPUnit_Framework_MockObject_MockObject|User
*/
private function getMockUnrestrictedNonAnonUserWithId( $id ) {
$mock = $this->getMockNonAnonUserWithId( $id );
$mock->expects( $this->any() )
->method( 'isAllowed' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'isAllowedAny' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'useRCPatrol' )
->will( $this->returnValue( true ) );
return $mock;
}
/**
* @param int $id
* @param string $notAllowedAction
* @return PHPUnit_Framework_MockObject_MockObject|User
*/
private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
$mock = $this->getMockNonAnonUserWithId( $id );
$mock->expects( $this->any() )
->method( 'isAllowed' )
->will( $this->returnCallback( function( $action ) use ( $notAllowedAction ) {
return $action !== $notAllowedAction;
} ) );
$mock->expects( $this->any() )
->method( 'isAllowedAny' )
->will( $this->returnCallback( function() use ( $notAllowedAction ) {
$actions = func_get_args();
return !in_array( $notAllowedAction, $actions );
} ) );
return $mock;
}
/**
* @param int $id
* @return PHPUnit_Framework_MockObject_MockObject|User
*/
private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
$mock = $this->getMockNonAnonUserWithId( $id );
$mock->expects( $this->any() )
->method( 'isAllowed' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'isAllowedAny' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'useRCPatrol' )
->will( $this->returnValue( false ) );
$mock->expects( $this->any() )
->method( 'useNPPatrol' )
->will( $this->returnValue( false ) );
return $mock;
}
private function getMockAnonUser() {
$mock = $this->getMock( User::class );
$mock->expects( $this->any() )
->method( 'isAnon' )
->will( $this->returnValue( true ) );
return $mock;
}
private function getFakeRow( array $rowValues ) {
$fakeRow = new stdClass();
foreach ( $rowValues as $valueName => $value ) {
$fakeRow->$valueName = $value;
}
return $fakeRow;
}
public function testGetWatchedItemsWithRecentChangeInfo() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
[
'rc_id',
'rc_namespace',
'rc_title',
'rc_timestamp',
'rc_type',
'rc_deleted',
'wl_notificationtimestamp',
'rc_cur_id',
'rc_this_oldid',
'rc_last_oldid',
],
[
'wl_user' => 1,
'(rc_this_oldid=page_latest) OR (rc_type=3)',
],
$this->isType( 'string' ),
[],
[
'watchlist' => [
'INNER JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
]
],
'page' => [
'LEFT JOIN',
'rc_cur_id=page_id',
],
]
)
->will( $this->returnValue( [
$this->getFakeRow( [
'rc_id' => 1,
'rc_namespace' => 0,
'rc_title' => 'Foo1',
'rc_timestamp' => '20151212010101',
'rc_type' => RC_NEW,
'rc_deleted' => 0,
'wl_notificationtimestamp' => '20151212010101',
] ),
$this->getFakeRow( [
'rc_id' => 2,
'rc_namespace' => 1,
'rc_title' => 'Foo2',
'rc_timestamp' => '20151212010102',
'rc_type' => RC_NEW,
'rc_deleted' => 0,
'wl_notificationtimestamp' => null,
] ),
] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo( $user );
$this->assertInternalType( 'array', $items );
$this->assertCount( 2, $items );
foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
$this->assertInstanceOf( WatchedItem::class, $watchedItem );
$this->assertInternalType( 'array', $recentChangeInfo );
}
$this->assertEquals(
new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
$items[0][0]
);
$this->assertEquals(
[
'rc_id' => 1,
'rc_namespace' => 0,
'rc_title' => 'Foo1',
'rc_timestamp' => '20151212010101',
'rc_type' => RC_NEW,
'rc_deleted' => 0,
],
$items[0][1]
);
$this->assertEquals(
new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
$items[1][0]
);
$this->assertEquals(
[
'rc_id' => 2,
'rc_namespace' => 1,
'rc_title' => 'Foo2',
'rc_timestamp' => '20151212010102',
'rc_type' => RC_NEW,
'rc_deleted' => 0,
],
$items[1][1]
);
}
public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
return [
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
[ 'rc_type', 'rc_minor', 'rc_bot' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
[ 'rc_user_text' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
[ 'rc_user' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
[ 'rc_comment' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
[ 'rc_patrolled', 'rc_log_type' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
[ 'rc_old_len', 'rc_new_len' ],
[],
[],
],
[
[ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
[ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
[],
[],
],
[
[ 'namespaceIds' => [ 0, 1 ] ],
[],
[ 'wl_namespace' => [ 0, 1 ] ],
[],
],
[
[ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
[],
[ 'wl_namespace' => [ 0, 1 ] ],
[],
],
[
[ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
[],
[ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
[],
],
[
[ 'dir' => WatchedItemQueryService::DIR_OLDER ],
[],
[],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
],
[
[ 'dir' => WatchedItemQueryService::DIR_NEWER ],
[],
[],
[ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
],
[
[ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
[],
[ "rc_timestamp <= '20151212010101'" ],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
],
[
[ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
[],
[ "rc_timestamp >= '20151212010101'" ],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
],
[
[
'dir' => WatchedItemQueryService::DIR_OLDER,
'start' => '20151212020101',
'end' => '20151212010101'
],
[],
[ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
],
[
[ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
[],
[ "rc_timestamp >= '20151212010101'" ],
[ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
],
[
[ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
[],
[ "rc_timestamp <= '20151212010101'" ],
[ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
],
[
[
'dir' => WatchedItemQueryService::DIR_NEWER,
'start' => '20151212010101',
'end' => '20151212020101'
],
[],
[ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
[ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
],
[
[ 'limit' => 10 ],
[],
[],
[ 'LIMIT' => 10 ],
],
[
[ 'limit' => "10; DROP TABLE watchlist;\n--" ],
[],
[],
[ 'LIMIT' => 10 ],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
[],
[ 'rc_minor != 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
[],
[ 'rc_minor = 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
[],
[ 'rc_bot != 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
[],
[ 'rc_bot = 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
[],
[ 'rc_user = 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
[],
[ 'rc_user != 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
[],
[ 'rc_patrolled != 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
[],
[ 'rc_patrolled = 0' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
[],
[ 'rc_timestamp >= wl_notificationtimestamp' ],
[],
],
[
[ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
[],
[ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
[],
],
[
[ 'onlyByUser' => 'SomeOtherUser' ],
[],
[ 'rc_user_text' => 'SomeOtherUser' ],
[],
],
[
[ 'notByUser' => 'SomeOtherUser' ],
[],
[ "rc_user_text != 'SomeOtherUser'" ],
[],
],
[
[ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_OLDER ],
[],
[
"(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
],
[
[ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_NEWER ],
[],
[
"(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
],
[ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
],
[
[
'startFrom' => [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
'dir' => WatchedItemQueryService::DIR_OLDER
],
[],
[
"(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
],
[ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
],
];
}
/**
* @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
array $options,
array $expectedExtraFields,
array $expectedExtraConds,
array $expectedDbOptions
) {
$expectedFields = array_merge(
[
'rc_id',
'rc_namespace',
'rc_title',
'rc_timestamp',
'rc_type',
'rc_deleted',
'wl_notificationtimestamp',
'rc_cur_id',
'rc_this_oldid',
'rc_last_oldid',
],
$expectedExtraFields
);
$expectedConds = array_merge(
[ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
$expectedExtraConds
);
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
$expectedFields,
$expectedConds,
$this->isType( 'string' ),
$expectedDbOptions,
[
'watchlist' => [
'INNER JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
]
],
'page' => [
'LEFT JOIN',
'rc_cur_id=page_id',
],
]
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
$this->assertEmpty( $items );
}
public function filterPatrolledOptionProvider() {
return [
[ WatchedItemQueryService::FILTER_PATROLLED ],
[ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
];
}
/**
* @dataProvider filterPatrolledOptionProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
$filtersOption
) {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
$this->isType( 'array' ),
[ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
$this->isType( 'string' ),
$this->isType( 'array' ),
$this->isType( 'array' )
)
->will( $this->returnValue( [] ) );
$user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$items = $queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'filters' => [ $filtersOption ] ]
);
$this->assertEmpty( $items );
}
public function mysqlIndexOptimizationProvider() {
return [
[
'mysql',
[],
[ "rc_timestamp > ''" ],
],
[
'mysql',
[ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
[ "rc_timestamp <= '20151212010101'" ],
],
[
'mysql',
[ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
[ "rc_timestamp >= '20151212010101'" ],
],
[
'postgres',
[],
[],
],
];
}
/**
* @dataProvider mysqlIndexOptimizationProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
$dbType,
array $options,
array $expectedExtraConds
) {
$commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
$conds = array_merge( $commonConds, $expectedExtraConds );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
$this->isType( 'array' ),
$conds,
$this->isType( 'string' ),
$this->isType( 'array' ),
$this->isType( 'array' )
)
->will( $this->returnValue( [] ) );
$mockDb->expects( $this->any() )
->method( 'getType' )
->will( $this->returnValue( $dbType ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
$this->assertEmpty( $items );
}
public function userPermissionRelatedExtraChecksProvider() {
return [
[
[],
'deletedhistory',
[
'(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
LogPage::DELETED_ACTION . ')'
],
],
[
[],
'suppressrevision',
[
'(rc_type != ' . RC_LOG . ') OR (' .
'(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
],
],
[
[],
'viewsuppressed',
[
'(rc_type != ' . RC_LOG . ') OR (' .
'(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
],
],
[
[ 'onlyByUser' => 'SomeOtherUser' ],
'deletedhistory',
[
'rc_user_text' => 'SomeOtherUser',
'(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
'(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
LogPage::DELETED_ACTION . ')'
],
],
[
[ 'onlyByUser' => 'SomeOtherUser' ],
'suppressrevision',
[
'rc_user_text' => 'SomeOtherUser',
'(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
'(rc_type != ' . RC_LOG . ') OR (' .
'(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
],
],
[
[ 'onlyByUser' => 'SomeOtherUser' ],
'viewsuppressed',
[
'rc_user_text' => 'SomeOtherUser',
'(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
'(rc_type != ' . RC_LOG . ') OR (' .
'(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
],
],
];
}
/**
* @dataProvider userPermissionRelatedExtraChecksProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
array $options,
$notAllowedAction,
array $expectedExtraConds
) {
$commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
$conds = array_merge( $commonConds, $expectedExtraConds );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
$this->isType( 'array' ),
$conds,
$this->isType( 'string' ),
$this->isType( 'array' ),
$this->isType( 'array' )
)
->will( $this->returnValue( [] ) );
$user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
$this->assertEmpty( $items );
}
public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist' ],
[
'rc_id',
'rc_namespace',
'rc_title',
'rc_timestamp',
'rc_type',
'rc_deleted',
'wl_notificationtimestamp',
'rc_cur_id',
'rc_this_oldid',
'rc_last_oldid',
],
[ 'wl_user' => 1, ],
$this->isType( 'string' ),
[],
[
'watchlist' => [
'INNER JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
]
],
]
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
$this->assertEmpty( $items );
}
public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
return [
[
[ 'rcTypes' => [ 1337 ] ],
'Bad value for parameter $options[\'rcTypes\']',
],
[
[ 'rcTypes' => [ 'edit' ] ],
'Bad value for parameter $options[\'rcTypes\']',
],
[
[ 'rcTypes' => [ RC_EDIT, 1337 ] ],
'Bad value for parameter $options[\'rcTypes\']',
],
[
[ 'dir' => 'foo' ],
'Bad value for parameter $options[\'dir\']',
],
[
[ 'start' => '20151212010101' ],
'Bad value for parameter $options[\'dir\']: must be provided',
],
[
[ 'end' => '20151212010101' ],
'Bad value for parameter $options[\'dir\']: must be provided',
],
[
[ 'startFrom' => [ '20151212010101', 123 ] ],
'Bad value for parameter $options[\'dir\']: must be provided',
],
[
[ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => '20151212010101' ],
'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
],
[
[ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => [ '20151212010101' ] ],
'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
],
[
[
'dir' => WatchedItemQueryService::DIR_OLDER,
'startFrom' => [ '20151212010101', 123, 'foo' ]
],
'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
],
[
[ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
'Bad value for parameter $options[\'watchlistOwnerToken\']',
],
[
[ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
'Bad value for parameter $options[\'watchlistOwner\']',
],
];
}
/**
* @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
array $options,
$expectedInExceptionMessage
) {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->never() )
->method( $this->anything() );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
$queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
}
public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist', 'page' ],
[
'rc_id',
'rc_namespace',
'rc_title',
'rc_timestamp',
'rc_type',
'rc_deleted',
'wl_notificationtimestamp',
'rc_cur_id',
],
[ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
$this->isType( 'string' ),
[],
[
'watchlist' => [
'INNER JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
]
],
'page' => [
'LEFT JOIN',
'rc_cur_id=page_id',
],
]
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'usedInGenerator' => true ]
);
$this->assertEmpty( $items );
}
public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
[ 'recentchanges', 'watchlist' ],
[
'rc_id',
'rc_namespace',
'rc_title',
'rc_timestamp',
'rc_type',
'rc_deleted',
'wl_notificationtimestamp',
'rc_this_oldid',
],
[ 'wl_user' => 1 ],
$this->isType( 'string' ),
[],
[
'watchlist' => [
'INNER JOIN',
[
'wl_namespace=rc_namespace',
'wl_title=rc_title'
]
],
]
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'usedInGenerator' => true, 'allRevisions' => true, ]
);
$this->assertEmpty( $items );
}
public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
$this->isType( 'array' ),
$this->isType( 'array' ),
[
'wl_user' => 2,
'(rc_this_oldid=page_latest) OR (rc_type=3)',
],
$this->isType( 'string' ),
$this->isType( 'array' ),
$this->isType( 'array' )
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
$otherUser->expects( $this->once() )
->method( 'getOption' )
->with( 'watchlisttoken' )
->willReturn( '0123456789abcdef' );
$items = $queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
);
$this->assertEmpty( $items );
}
public function invalidWatchlistTokenProvider() {
return [
[ 'wrongToken' ],
[ '' ],
];
}
/**
* @dataProvider invalidWatchlistTokenProvider
*/
public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->never() )
->method( $this->anything() );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
$otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
$otherUser->expects( $this->once() )
->method( 'getOption' )
->with( 'watchlisttoken' )
->willReturn( '0123456789abcdef' );
$this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
$queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
);
}
public function testGetWatchedItemsForUser() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
'watchlist',
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
[ 'wl_user' => 1 ]
)
->will( $this->returnValue( [
$this->getFakeRow( [
'wl_namespace' => 0,
'wl_title' => 'Foo1',
'wl_notificationtimestamp' => '20151212010101',
] ),
$this->getFakeRow( [
'wl_namespace' => 1,
'wl_title' => 'Foo2',
'wl_notificationtimestamp' => null,
] ),
] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$user = $this->getMockNonAnonUserWithId( 1 );
$items = $queryService->getWatchedItemsForUser( $user );
$this->assertInternalType( 'array', $items );
$this->assertCount( 2, $items );
$this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
$this->assertEquals(
new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
$items[0]
);
$this->assertEquals(
new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
$items[1]
);
}
public function provideGetWatchedItemsForUserOptions() {
return [
[
[ 'namespaceIds' => [ 0, 1 ], ],
[ 'wl_namespace' => [ 0, 1 ], ],
[]
],
[
[ 'sort' => WatchedItemQueryService::SORT_ASC, ],
[],
[ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
],
[
[
'namespaceIds' => [ 0 ],
'sort' => WatchedItemQueryService::SORT_ASC,
],
[ 'wl_namespace' => [ 0 ], ],
[ 'ORDER BY' => 'wl_title ASC' ]
],
[
[ 'limit' => 10 ],
[],
[ 'LIMIT' => 10 ]
],
[
[
'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
'limit' => "10; DROP TABLE watchlist;\n--",
],
[ 'wl_namespace' => [ 0, 1 ], ],
[ 'LIMIT' => 10 ]
],
[
[ 'filter' => WatchedItemQueryService::FILTER_CHANGED ],
[ 'wl_notificationtimestamp IS NOT NULL' ],
[]
],
[
[ 'filter' => WatchedItemQueryService::FILTER_NOT_CHANGED ],
[ 'wl_notificationtimestamp IS NULL' ],
[]
],
[
[ 'sort' => WatchedItemQueryService::SORT_DESC, ],
[],
[ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
],
[
[
'namespaceIds' => [ 0 ],
'sort' => WatchedItemQueryService::SORT_DESC,
],
[ 'wl_namespace' => [ 0 ], ],
[ 'ORDER BY' => 'wl_title DESC' ]
],
];
}
/**
* @dataProvider provideGetWatchedItemsForUserOptions
*/
public function testGetWatchedItemsForUser_optionsAndEmptyResult(
array $options,
array $expectedConds,
array $expectedDbOptions
) {
$mockDb = $this->getMockDb();
$user = $this->getMockNonAnonUserWithId( 1 );
$expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
'watchlist',
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
$expectedConds,
$this->isType( 'string' ),
$expectedDbOptions
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$items = $queryService->getWatchedItemsForUser( $user, $options );
$this->assertEmpty( $items );
}
public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
return [
[
[
'from' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_ASC
],
[ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
[ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
],
[
[
'from' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_DESC,
],
[ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
[ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
],
[
[
'until' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_ASC
],
[ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
[ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
],
[
[
'until' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_DESC
],
[ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
[ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
],
[
[
'from' => new TitleValue( 0, 'AnotherDbKey' ),
'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_ASC
],
[
"(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
"(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
"(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
],
[ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
],
[
[
'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
'until' => new TitleValue( 0, 'AnotherDbKey' ),
'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
'sort' => WatchedItemQueryService::SORT_DESC
],
[
"(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
"(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
"(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
],
[ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
],
];
}
/**
* @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
*/
public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
array $options,
array $expectedConds,
array $expectedDbOptions
) {
$user = $this->getMockNonAnonUserWithId( 1 );
$expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->any() )
->method( 'addQuotes' )
->will( $this->returnCallback( function( $value ) {
return "'$value'";
} ) );
$mockDb->expects( $this->any() )
->method( 'makeList' )
->with(
$this->isType( 'array' ),
$this->isType( 'int' )
)
->will( $this->returnCallback( function( $a, $conj ) {
$sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
return join( $sqlConj, array_map( function( $s ) {
return '(' . $s . ')';
}, $a
) );
} ) );
$mockDb->expects( $this->once() )
->method( 'select' )
->with(
'watchlist',
[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
$expectedConds,
$this->isType( 'string' ),
$expectedDbOptions
)
->will( $this->returnValue( [] ) );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$items = $queryService->getWatchedItemsForUser( $user, $options );
$this->assertEmpty( $items );
}
public function getWatchedItemsForUserInvalidOptionsProvider() {
return [
[
[ 'sort' => 'foo' ],
'Bad value for parameter $options[\'sort\']'
],
[
[ 'filter' => 'foo' ],
'Bad value for parameter $options[\'filter\']'
],
[
[ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
'Bad value for parameter $options[\'sort\']: must be provided'
],
[
[ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
'Bad value for parameter $options[\'sort\']: must be provided'
],
[
[ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
'Bad value for parameter $options[\'sort\']: must be provided'
],
];
}
/**
* @dataProvider getWatchedItemsForUserInvalidOptionsProvider
*/
public function testGetWatchedItemsForUser_invalidOptionThrowsException(
array $options,
$expectedInExceptionMessage
) {
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
$this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
$queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
}
public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
$mockDb = $this->getMockDb();
$mockDb->expects( $this->never() )
->method( $this->anything() );
$queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
$items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
$this->assertEmpty( $items );
}
}