3 use MediaWiki\Permissions\PermissionManager
;
4 use MediaWiki\User\UserIdentityValue
;
5 use Wikimedia\Rdbms\IDatabase
;
6 use Wikimedia\Rdbms\LoadBalancer
;
7 use Wikimedia\TestingAccessWrapper
;
10 * @covers WatchedItemQueryService
12 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
15 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
17 private function getMockCommentStore() {
18 $mockStore = $this->getMockBuilder( CommentStore
::class )
19 ->disableOriginalConstructor()
21 $mockStore->expects( $this->any() )
22 ->method( 'getFields' )
23 ->willReturn( [ 'commentstore' => 'fields' ] );
24 $mockStore->expects( $this->any() )
27 'tables' => [ 'commentstore' => 'table' ],
28 'fields' => [ 'commentstore' => 'field' ],
29 'joins' => [ 'commentstore' => 'join' ],
35 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
37 private function getMockActorMigration() {
38 $mockStore = $this->getMockBuilder( ActorMigration
::class )
39 ->disableOriginalConstructor()
41 $mockStore->expects( $this->any() )
44 'tables' => [ 'actormigration' => 'table' ],
46 'rc_user' => 'actormigration_user',
47 'rc_user_text' => 'actormigration_user_text',
48 'rc_actor' => 'actormigration_actor',
50 'joins' => [ 'actormigration' => 'join' ],
52 $mockStore->expects( $this->any() )
53 ->method( 'getWhere' )
55 'tables' => [ 'actormigration' => 'table' ],
56 'conds' => 'actormigration_conds',
57 'joins' => [ 'actormigration' => 'join' ],
59 $mockStore->expects( $this->any() )
61 ->willReturn( 'actormigration is anon' );
62 $mockStore->expects( $this->any() )
63 ->method( 'isNotAnon' )
64 ->willReturn( 'actormigration is not anon' );
69 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
70 * @param PHPUnit_Framework_MockObject_MockObject|PermissionManager $mockPM
71 * @return WatchedItemQueryService
73 private function newService( $mockDb, $mockPM = null ) {
74 return new WatchedItemQueryService(
75 $this->getMockLoadBalancer( $mockDb ),
76 $this->getMockCommentStore(),
77 $this->getMockActorMigration(),
78 $this->getMockWatchedItemStore(),
79 $mockPM ?
: $this->getMockPermissionManager()
84 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
86 private function getMockDb() {
87 $mock = $this->createMock( IDatabase
::class );
89 $mock->expects( $this->any() )
90 ->method( 'makeList' )
92 $this->isType( 'array' ),
93 $this->isType( 'int' )
95 ->will( $this->returnCallback( function ( $a, $conj ) {
96 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
98 foreach ( $a as $k => $v ) {
101 } elseif ( is_array( $v ) ) {
102 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
104 $conds[] = "($k = '$v')";
107 return implode( $sqlConj, $conds );
110 $mock->expects( $this->any() )
111 ->method( 'addQuotes' )
112 ->will( $this->returnCallback( function ( $value ) {
116 $mock->expects( $this->any() )
117 ->method( 'timestamp' )
118 ->will( $this->returnArgument( 0 ) );
120 $mock->expects( $this->any() )
122 ->willReturnCallback( function ( $a, $b ) {
130 * @param PHPUnit_Framework_MockObject_MockObject|IDatabase $mockDb
131 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
133 private function getMockLoadBalancer( $mockDb ) {
134 $mock = $this->getMockBuilder( LoadBalancer
::class )
135 ->disableOriginalConstructor()
137 $mock->expects( $this->any() )
138 ->method( 'getConnectionRef' )
140 ->will( $this->returnValue( $mockDb ) );
145 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
147 private function getMockWatchedItemStore() {
148 $mock = $this->getMockBuilder( WatchedItemStore
::class )
149 ->disableOriginalConstructor()
151 $mock->expects( $this->any() )
152 ->method( 'getLatestNotificationTimestamp' )
153 ->will( $this->returnCallback( function ( $timestamp ) {
160 * @param string $notAllowedAction
161 * @return PHPUnit_Framework_MockObject_MockObject|PermissionManager
163 private function getMockPermissionManager( $notAllowedAction = null ) {
164 $mock = $this->getMockBuilder( PermissionManager
::class )
165 ->disableOriginalConstructor()
167 $mock->method( 'userHasRight' )
168 ->will( $this->returnCallback( function ( $user, $action ) use ( $notAllowedAction ) {
169 return $action !== $notAllowedAction;
171 $mock->method( 'userHasAnyRight' )
172 ->will( $this->returnCallback( function ( $user, ...$actions ) use ( $notAllowedAction ) {
173 return !in_array( $notAllowedAction, $actions );
180 * @param string[] $extraMethods Extra methods that are expected might be called
181 * @return PHPUnit_Framework_MockObject_MockObject|User
183 private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
184 $mock = $this->getMockBuilder( User
::class )->getMock();
185 $mock->method( 'isRegistered' )->willReturn( true );
186 $mock->method( 'getId' )->willReturn( $id );
187 $methods = array_merge( [
191 $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) );
197 * @param string[] $extraMethods Extra methods that are expected might be called
198 * @return PHPUnit_Framework_MockObject_MockObject|User
200 private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
201 $mock = $this->getMockNonAnonUserWithId( $id,
202 array_merge( [ 'useRCPatrol' ], $extraMethods ) );
203 $mock->method( 'useRCPatrol' )->willReturn( true );
209 * @param string $notAllowedAction
210 * @return PHPUnit_Framework_MockObject_MockObject|User
212 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id ) {
213 $mock = $this->getMockNonAnonUserWithId( $id,
214 [ 'useRCPatrol', 'useNPPatrol' ] );
215 $mock->method( 'useRCPatrol' )->willReturn( false );
216 $mock->method( 'useNPPatrol' )->willReturn( false );
223 * @return PHPUnit_Framework_MockObject_MockObject|User
225 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
226 $mock = $this->getMockNonAnonUserWithId( $id,
227 [ 'useRCPatrol', 'useNPPatrol' ] );
228 $mock->expects( $this->any() )
229 ->method( 'useRCPatrol' )
230 ->will( $this->returnValue( false ) );
231 $mock->expects( $this->any() )
232 ->method( 'useNPPatrol' )
233 ->will( $this->returnValue( false ) );
238 private function getFakeRow( array $rowValues ) {
239 $fakeRow = new stdClass();
240 foreach ( $rowValues as $valueName => $value ) {
241 $fakeRow->$valueName = $value;
246 public function testGetWatchedItemsWithRecentChangeInfo() {
247 $mockDb = $this->getMockDb();
248 $mockDb->expects( $this->once() )
251 [ 'recentchanges', 'watchlist', 'page' ],
259 'wl_notificationtimestamp',
266 '(rc_this_oldid=page_latest) OR (rc_type=3)',
268 $this->isType( 'string' ),
276 'wl_namespace=rc_namespace',
286 ->will( $this->returnValue( [
290 'rc_title' => 'Foo1',
291 'rc_timestamp' => '20151212010101',
294 'wl_notificationtimestamp' => '20151212010101',
299 'rc_title' => 'Foo2',
300 'rc_timestamp' => '20151212010102',
303 'wl_notificationtimestamp' => null,
308 'rc_title' => 'Foo3',
309 'rc_timestamp' => '20151212010103',
312 'wl_notificationtimestamp' => null,
316 $queryService = $this->newService( $mockDb );
317 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
320 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
321 $user, [ 'limit' => 2 ], $startFrom
324 $this->assertInternalType( 'array', $items );
325 $this->assertCount( 2, $items );
327 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
328 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
329 $this->assertInternalType( 'array', $recentChangeInfo );
333 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
340 'rc_title' => 'Foo1',
341 'rc_timestamp' => '20151212010101',
349 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
356 'rc_title' => 'Foo2',
357 'rc_timestamp' => '20151212010102',
364 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
367 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
368 $mockDb = $this->getMockDb();
369 $mockDb->expects( $this->once() )
372 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
380 'wl_notificationtimestamp',
384 'extension_dummy_field',
388 '(rc_this_oldid=page_latest) OR (rc_type=3)',
389 'extension_dummy_cond',
391 $this->isType( 'string' ),
393 'extension_dummy_option',
399 'wl_namespace=rc_namespace',
407 'extension_dummy_join_cond' => [],
410 ->will( $this->returnValue( [
414 'rc_title' => 'Foo1',
415 'rc_timestamp' => '20151212010101',
418 'wl_notificationtimestamp' => '20151212010101',
423 'rc_title' => 'Foo2',
424 'rc_timestamp' => '20151212010102',
427 'wl_notificationtimestamp' => null,
431 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
433 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
435 $mockExtension->expects( $this->once() )
436 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
438 $this->identicalTo( $user ),
439 $this->isType( 'array' ),
440 $this->isInstanceOf( IDatabase
::class ),
441 $this->isType( 'array' ),
442 $this->isType( 'array' ),
443 $this->isType( 'array' ),
444 $this->isType( 'array' ),
445 $this->isType( 'array' )
447 ->will( $this->returnCallback( function (
448 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
450 $tables[] = 'extension_dummy_table';
451 $fields[] = 'extension_dummy_field';
452 $conds[] = 'extension_dummy_cond';
453 $dbOptions[] = 'extension_dummy_option';
454 $joinConds['extension_dummy_join_cond'] = [];
456 $mockExtension->expects( $this->once() )
457 ->method( 'modifyWatchedItemsWithRCInfo' )
459 $this->identicalTo( $user ),
460 $this->isType( 'array' ),
461 $this->isInstanceOf( IDatabase
::class ),
462 $this->isType( 'array' ),
464 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
466 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
467 foreach ( $items as $i => &$item ) {
468 $item[1]['extension_dummy_field'] = $i;
472 $this->assertNull( $startFrom );
473 $startFrom = [ '20160203123456', 42 ];
476 $queryService = $this->newService( $mockDb );
477 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
480 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
481 $user, [], $startFrom
484 $this->assertInternalType( 'array', $items );
485 $this->assertCount( 2, $items );
487 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
488 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
489 $this->assertInternalType( 'array', $recentChangeInfo );
493 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
500 'rc_title' => 'Foo1',
501 'rc_timestamp' => '20151212010101',
504 'extension_dummy_field' => 0,
510 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
517 'rc_title' => 'Foo2',
518 'rc_timestamp' => '20151212010102',
521 'extension_dummy_field' => 1,
526 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
529 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
532 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
535 [ 'rc_type', 'rc_minor', 'rc_bot' ],
541 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
543 [ 'actormigration' => 'table' ],
544 [ 'rc_user_text' => 'actormigration_user_text' ],
547 [ 'actormigration' => 'join' ],
550 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
552 [ 'actormigration' => 'table' ],
553 [ 'rc_user' => 'actormigration_user' ],
556 [ 'actormigration' => 'join' ],
559 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
561 [ 'commentstore' => 'table' ],
562 [ 'commentstore' => 'field' ],
565 [ 'commentstore' => 'join' ],
568 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
571 [ 'rc_patrolled', 'rc_log_type' ],
577 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
580 [ 'rc_old_len', 'rc_new_len' ],
586 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
589 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
595 [ 'namespaceIds' => [ 0, 1 ] ],
599 [ 'wl_namespace' => [ 0, 1 ] ],
604 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
608 [ 'wl_namespace' => [ 0, 1 ] ],
613 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
617 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
622 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
627 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
631 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
636 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
640 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
644 [ "rc_timestamp <= '20151212010101'" ],
645 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
649 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
653 [ "rc_timestamp >= '20151212010101'" ],
654 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
659 'dir' => WatchedItemQueryService
::DIR_OLDER
,
660 'start' => '20151212020101',
661 'end' => '20151212010101'
666 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
667 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
671 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
675 [ "rc_timestamp >= '20151212010101'" ],
676 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
680 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
684 [ "rc_timestamp <= '20151212010101'" ],
685 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
690 'dir' => WatchedItemQueryService
::DIR_NEWER
,
691 'start' => '20151212010101',
692 'end' => '20151212020101'
697 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
698 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
711 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
720 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
729 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
738 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
747 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
756 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
758 [ 'actormigration' => 'table' ],
760 [ 'actormigration is anon' ],
762 [ 'actormigration' => 'join' ],
765 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
767 [ 'actormigration' => 'table' ],
769 [ 'actormigration is not anon' ],
771 [ 'actormigration' => 'join' ],
774 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
778 [ 'rc_patrolled != 0' ],
783 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
787 [ 'rc_patrolled' => 0 ],
792 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
796 [ 'rc_timestamp >= wl_notificationtimestamp' ],
801 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
805 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
810 [ 'onlyByUser' => 'SomeOtherUser' ],
812 [ 'actormigration' => 'table' ],
814 [ 'actormigration_conds' ],
816 [ 'actormigration' => 'join' ],
819 [ 'notByUser' => 'SomeOtherUser' ],
821 [ 'actormigration' => 'table' ],
823 [ 'NOT(actormigration_conds)' ],
825 [ 'actormigration' => 'join' ],
828 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
829 [ '20151212010101', 123 ],
833 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
835 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
839 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
840 [ '20151212010101', 123 ],
844 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
846 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
850 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
851 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
855 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
857 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
864 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
866 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
869 array $expectedExtraTables,
870 array $expectedExtraFields,
871 array $expectedExtraConds,
872 array $expectedDbOptions,
873 array $expectedExtraJoinConds
875 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
876 $expectedFields = array_merge(
884 'wl_notificationtimestamp',
892 $expectedConds = array_merge(
893 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
896 $expectedJoinConds = array_merge(
901 'wl_namespace=rc_namespace',
910 $expectedExtraJoinConds
913 $mockDb = $this->getMockDb();
914 $mockDb->expects( $this->once() )
920 $this->isType( 'string' ),
924 ->will( $this->returnValue( [] ) );
926 $queryService = $this->newService( $mockDb );
927 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
929 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
931 $this->assertEmpty( $items );
932 $this->assertNull( $startFrom );
935 public function filterPatrolledOptionProvider() {
937 [ WatchedItemQueryService
::FILTER_PATROLLED
],
938 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
943 * @dataProvider filterPatrolledOptionProvider
945 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
948 $mockDb = $this->getMockDb();
949 $mockDb->expects( $this->once() )
952 [ 'recentchanges', 'watchlist', 'page' ],
953 $this->isType( 'array' ),
954 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
955 $this->isType( 'string' ),
956 $this->isType( 'array' ),
957 $this->isType( 'array' )
959 ->will( $this->returnValue( [] ) );
961 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
963 $queryService = $this->newService( $mockDb );
964 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
966 [ 'filters' => [ $filtersOption ] ]
969 $this->assertEmpty( $items );
972 public function mysqlIndexOptimizationProvider() {
977 [ "rc_timestamp > ''" ],
981 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
982 [ "rc_timestamp <= '20151212010101'" ],
986 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
987 [ "rc_timestamp >= '20151212010101'" ],
998 * @dataProvider mysqlIndexOptimizationProvider
1000 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1003 array $expectedExtraConds
1005 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1006 $conds = array_merge( $commonConds, $expectedExtraConds );
1008 $mockDb = $this->getMockDb();
1009 $mockDb->expects( $this->once() )
1010 ->method( 'select' )
1012 [ 'recentchanges', 'watchlist', 'page' ],
1013 $this->isType( 'array' ),
1015 $this->isType( 'string' ),
1016 $this->isType( 'array' ),
1017 $this->isType( 'array' )
1019 ->will( $this->returnValue( [] ) );
1020 $mockDb->expects( $this->any() )
1021 ->method( 'getType' )
1022 ->will( $this->returnValue( $dbType ) );
1024 $queryService = $this->newService( $mockDb );
1025 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1027 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1029 $this->assertEmpty( $items );
1032 public function userPermissionRelatedExtraChecksProvider() {
1039 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1040 LogPage
::DELETED_ACTION
. ')'
1049 '(rc_type != ' . RC_LOG
. ') OR (' .
1050 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1051 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1060 '(rc_type != ' . RC_LOG
. ') OR (' .
1061 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1062 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1067 [ 'onlyByUser' => 'SomeOtherUser' ],
1069 [ 'actormigration' => 'table' ],
1071 'actormigration_conds',
1072 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1073 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1074 LogPage
::DELETED_ACTION
. ')'
1076 [ 'actormigration' => 'join' ],
1079 [ 'onlyByUser' => 'SomeOtherUser' ],
1081 [ 'actormigration' => 'table' ],
1083 'actormigration_conds',
1084 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1085 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1086 '(rc_type != ' . RC_LOG
. ') OR (' .
1087 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1088 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1090 [ 'actormigration' => 'join' ],
1093 [ 'onlyByUser' => 'SomeOtherUser' ],
1095 [ 'actormigration' => 'table' ],
1097 'actormigration_conds',
1098 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1099 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1100 '(rc_type != ' . RC_LOG
. ') OR (' .
1101 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1102 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1104 [ 'actormigration' => 'join' ],
1110 * @dataProvider userPermissionRelatedExtraChecksProvider
1112 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1115 array $expectedExtraTables,
1116 array $expectedExtraConds,
1117 array $expectedExtraJoins
1119 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1120 $conds = array_merge( $commonConds, $expectedExtraConds );
1122 $mockDb = $this->getMockDb();
1123 $mockDb->expects( $this->once() )
1124 ->method( 'select' )
1126 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1127 $this->isType( 'array' ),
1129 $this->isType( 'string' ),
1130 $this->isType( 'array' ),
1132 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1133 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1134 ], $expectedExtraJoins )
1136 ->will( $this->returnValue( [] ) );
1138 $permissionManager = $this->getMockPermissionManager( $notAllowedAction );
1139 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1 );
1141 $queryService = $this->newService( $mockDb, $permissionManager );
1142 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1144 $this->assertEmpty( $items );
1147 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1148 $mockDb = $this->getMockDb();
1149 $mockDb->expects( $this->once() )
1150 ->method( 'select' )
1152 [ 'recentchanges', 'watchlist' ],
1160 'wl_notificationtimestamp',
1166 [ 'wl_user' => 1, ],
1167 $this->isType( 'string' ),
1173 'wl_namespace=rc_namespace',
1179 ->will( $this->returnValue( [] ) );
1181 $queryService = $this->newService( $mockDb );
1182 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1184 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1186 $this->assertEmpty( $items );
1189 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1192 [ 'rcTypes' => [ 1337 ] ],
1194 'Bad value for parameter $options[\'rcTypes\']',
1197 [ 'rcTypes' => [ 'edit' ] ],
1199 'Bad value for parameter $options[\'rcTypes\']',
1202 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1204 'Bad value for parameter $options[\'rcTypes\']',
1209 'Bad value for parameter $options[\'dir\']',
1212 [ 'start' => '20151212010101' ],
1214 'Bad value for parameter $options[\'dir\']: must be provided',
1217 [ 'end' => '20151212010101' ],
1219 'Bad value for parameter $options[\'dir\']: must be provided',
1223 [ '20151212010101', 123 ],
1224 'Bad value for parameter $options[\'dir\']: must be provided',
1227 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1229 'Bad value for parameter $startFrom: must be a two-element array',
1232 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1233 [ '20151212010101' ],
1234 'Bad value for parameter $startFrom: must be a two-element array',
1237 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1238 [ '20151212010101', 123, 'foo' ],
1239 'Bad value for parameter $startFrom: must be a two-element array',
1242 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1244 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1247 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1249 'Bad value for parameter $options[\'watchlistOwner\']',
1255 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1257 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1260 $expectedInExceptionMessage
1262 $mockDb = $this->getMockDb();
1263 $mockDb->expects( $this->never() )
1264 ->method( $this->anything() );
1266 $queryService = $this->newService( $mockDb );
1267 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1269 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1270 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1273 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1274 $mockDb = $this->getMockDb();
1275 $mockDb->expects( $this->once() )
1276 ->method( 'select' )
1278 [ 'recentchanges', 'watchlist', 'page' ],
1286 'wl_notificationtimestamp',
1289 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1290 $this->isType( 'string' ),
1296 'wl_namespace=rc_namespace',
1302 'rc_cur_id=page_id',
1306 ->will( $this->returnValue( [] ) );
1308 $queryService = $this->newService( $mockDb );
1309 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1311 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1313 [ 'usedInGenerator' => true ]
1316 $this->assertEmpty( $items );
1319 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1320 $mockDb = $this->getMockDb();
1321 $mockDb->expects( $this->once() )
1322 ->method( 'select' )
1324 [ 'recentchanges', 'watchlist' ],
1332 'wl_notificationtimestamp',
1336 $this->isType( 'string' ),
1342 'wl_namespace=rc_namespace',
1348 ->will( $this->returnValue( [] ) );
1350 $queryService = $this->newService( $mockDb );
1351 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1353 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1355 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1358 $this->assertEmpty( $items );
1361 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1362 $mockDb = $this->getMockDb();
1363 $mockDb->expects( $this->once() )
1364 ->method( 'select' )
1366 $this->isType( 'array' ),
1367 $this->isType( 'array' ),
1370 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1372 $this->isType( 'string' ),
1373 $this->isType( 'array' ),
1374 $this->isType( 'array' )
1376 ->will( $this->returnValue( [] ) );
1378 $queryService = $this->newService( $mockDb );
1379 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1380 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1381 $otherUser->expects( $this->once() )
1382 ->method( 'getOption' )
1383 ->with( 'watchlisttoken' )
1384 ->willReturn( '0123456789abcdef' );
1386 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1388 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1391 $this->assertEmpty( $items );
1394 public function invalidWatchlistTokenProvider() {
1402 * @dataProvider invalidWatchlistTokenProvider
1404 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1405 $mockDb = $this->getMockDb();
1406 $mockDb->expects( $this->never() )
1407 ->method( $this->anything() );
1409 $queryService = $this->newService( $mockDb );
1410 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1411 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1412 $otherUser->expects( $this->once() )
1413 ->method( 'getOption' )
1414 ->with( 'watchlisttoken' )
1415 ->willReturn( '0123456789abcdef' );
1417 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1418 $queryService->getWatchedItemsWithRecentChangeInfo(
1420 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1424 public function testGetWatchedItemsForUser() {
1425 $mockDb = $this->getMockDb();
1426 $mockDb->expects( $this->once() )
1427 ->method( 'select' )
1430 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1433 ->will( $this->returnValue( [
1434 $this->getFakeRow( [
1435 'wl_namespace' => 0,
1436 'wl_title' => 'Foo1',
1437 'wl_notificationtimestamp' => '20151212010101',
1439 $this->getFakeRow( [
1440 'wl_namespace' => 1,
1441 'wl_title' => 'Foo2',
1442 'wl_notificationtimestamp' => null,
1446 $queryService = $this->newService( $mockDb );
1447 $user = $this->getMockNonAnonUserWithId( 1 );
1449 $items = $queryService->getWatchedItemsForUser( $user );
1451 $this->assertInternalType( 'array', $items );
1452 $this->assertCount( 2, $items );
1453 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1454 $this->assertEquals(
1455 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1458 $this->assertEquals(
1459 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1464 public function provideGetWatchedItemsForUserOptions() {
1467 [ 'namespaceIds' => [ 0, 1 ], ],
1468 [ 'wl_namespace' => [ 0, 1 ], ],
1472 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1474 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1478 'namespaceIds' => [ 0 ],
1479 'sort' => WatchedItemQueryService
::SORT_ASC
,
1481 [ 'wl_namespace' => [ 0 ], ],
1482 [ 'ORDER BY' => 'wl_title ASC' ]
1491 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1492 'limit' => "10; DROP TABLE watchlist;\n--",
1494 [ 'wl_namespace' => [ 0, 1 ], ],
1498 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1499 [ 'wl_notificationtimestamp IS NOT NULL' ],
1503 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1504 [ 'wl_notificationtimestamp IS NULL' ],
1508 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1510 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1514 'namespaceIds' => [ 0 ],
1515 'sort' => WatchedItemQueryService
::SORT_DESC
,
1517 [ 'wl_namespace' => [ 0 ], ],
1518 [ 'ORDER BY' => 'wl_title DESC' ]
1524 * @dataProvider provideGetWatchedItemsForUserOptions
1526 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1528 array $expectedConds,
1529 array $expectedDbOptions
1531 $mockDb = $this->getMockDb();
1532 $user = $this->getMockNonAnonUserWithId( 1 );
1534 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1535 $mockDb->expects( $this->once() )
1536 ->method( 'select' )
1539 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1541 $this->isType( 'string' ),
1544 ->will( $this->returnValue( [] ) );
1546 $queryService = $this->newService( $mockDb );
1548 $items = $queryService->getWatchedItemsForUser( $user, $options );
1549 $this->assertEmpty( $items );
1552 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1556 'from' => new TitleValue( 0, 'SomeDbKey' ),
1557 'sort' => WatchedItemQueryService
::SORT_ASC
1559 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1560 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1564 'from' => new TitleValue( 0, 'SomeDbKey' ),
1565 'sort' => WatchedItemQueryService
::SORT_DESC
,
1567 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1568 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1572 'until' => new TitleValue( 0, 'SomeDbKey' ),
1573 'sort' => WatchedItemQueryService
::SORT_ASC
1575 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1576 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1580 'until' => new TitleValue( 0, 'SomeDbKey' ),
1581 'sort' => WatchedItemQueryService
::SORT_DESC
1583 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1584 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1588 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1589 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1590 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1591 'sort' => WatchedItemQueryService
::SORT_ASC
1594 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1595 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1596 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1598 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1602 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1603 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1604 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1605 'sort' => WatchedItemQueryService
::SORT_DESC
1608 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1609 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1610 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1612 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1618 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1620 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1622 array $expectedConds,
1623 array $expectedDbOptions
1625 $user = $this->getMockNonAnonUserWithId( 1 );
1627 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1629 $mockDb = $this->getMockDb();
1630 $mockDb->expects( $this->any() )
1631 ->method( 'addQuotes' )
1632 ->will( $this->returnCallback( function ( $value ) {
1635 $mockDb->expects( $this->any() )
1636 ->method( 'makeList' )
1638 $this->isType( 'array' ),
1639 $this->isType( 'int' )
1641 ->will( $this->returnCallback( function ( $a, $conj ) {
1642 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1643 return implode( $sqlConj, array_map( function ( $s ) {
1644 return '(' . $s . ')';
1648 $mockDb->expects( $this->once() )
1649 ->method( 'select' )
1652 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1654 $this->isType( 'string' ),
1657 ->will( $this->returnValue( [] ) );
1659 $queryService = $this->newService( $mockDb );
1661 $items = $queryService->getWatchedItemsForUser( $user, $options );
1662 $this->assertEmpty( $items );
1665 public function getWatchedItemsForUserInvalidOptionsProvider() {
1668 [ 'sort' => 'foo' ],
1669 'Bad value for parameter $options[\'sort\']'
1672 [ 'filter' => 'foo' ],
1673 'Bad value for parameter $options[\'filter\']'
1676 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1677 'Bad value for parameter $options[\'sort\']: must be provided'
1680 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1681 'Bad value for parameter $options[\'sort\']: must be provided'
1684 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1685 'Bad value for parameter $options[\'sort\']: must be provided'
1691 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1693 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1695 $expectedInExceptionMessage
1697 $queryService = $this->newService( $this->getMockDb() );
1699 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1700 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1703 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1704 $mockDb = $this->getMockDb();
1706 $mockDb->expects( $this->never() )
1707 ->method( $this->anything() );
1709 $queryService = $this->newService( $mockDb );
1711 $items = $queryService->getWatchedItemsForUser(
1712 new UserIdentityValue( 0, 'AnonUser', 0 ) );
1713 $this->assertEmpty( $items );