3 use Wikimedia\Rdbms\LoadBalancer
;
4 use Wikimedia\TestingAccessWrapper
;
7 * @covers WatchedItemQueryService
9 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
11 use MediaWikiCoversValidator
;
14 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
16 private function getMockCommentStore() {
17 $mockStore = $this->getMockBuilder( CommentStore
::class )
18 ->disableOriginalConstructor()
20 $mockStore->expects( $this->any() )
21 ->method( 'getFields' )
22 ->willReturn( [ 'commentstore' => 'fields' ] );
23 $mockStore->expects( $this->any() )
26 'tables' => [ 'commentstore' => 'table' ],
27 'fields' => [ 'commentstore' => 'field' ],
28 'joins' => [ 'commentstore' => 'join' ],
34 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
36 private function getMockActorMigration() {
37 $mockStore = $this->getMockBuilder( ActorMigration
::class )
38 ->disableOriginalConstructor()
40 $mockStore->expects( $this->any() )
43 'tables' => [ 'actormigration' => 'table' ],
45 'rc_user' => 'actormigration_user',
46 'rc_user_text' => 'actormigration_user_text',
47 'rc_actor' => 'actormigration_actor',
49 'joins' => [ 'actormigration' => 'join' ],
51 $mockStore->expects( $this->any() )
52 ->method( 'getWhere' )
54 'tables' => [ 'actormigration' => 'table' ],
55 'conds' => 'actormigration_conds',
56 'joins' => [ 'actormigration' => 'join' ],
58 $mockStore->expects( $this->any() )
60 ->willReturn( 'actormigration is anon' );
61 $mockStore->expects( $this->any() )
62 ->method( 'isNotAnon' )
63 ->willReturn( 'actormigration is not anon' );
68 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
69 * @return WatchedItemQueryService
71 private function newService( $mockDb ) {
72 return new WatchedItemQueryService(
73 $this->getMockLoadBalancer( $mockDb ),
74 $this->getMockCommentStore(),
75 $this->getMockActorMigration()
80 * @return PHPUnit_Framework_MockObject_MockObject|Database
82 private function getMockDb() {
83 $mock = $this->getMockBuilder( Database
::class )
84 ->disableOriginalConstructor()
87 $mock->expects( $this->any() )
88 ->method( 'makeList' )
90 $this->isType( 'array' ),
91 $this->isType( 'int' )
93 ->will( $this->returnCallback( function ( $a, $conj ) {
94 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
96 foreach ( $a as $k => $v ) {
99 } elseif ( is_array( $v ) ) {
100 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
102 $conds[] = "($k = '$v')";
105 return implode( $sqlConj, $conds );
108 $mock->expects( $this->any() )
109 ->method( 'addQuotes' )
110 ->will( $this->returnCallback( function ( $value ) {
114 $mock->expects( $this->any() )
115 ->method( 'timestamp' )
116 ->will( $this->returnArgument( 0 ) );
118 $mock->expects( $this->any() )
120 ->willReturnCallback( function ( $a, $b ) {
128 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
129 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
131 private function getMockLoadBalancer( $mockDb ) {
132 $mock = $this->getMockBuilder( LoadBalancer
::class )
133 ->disableOriginalConstructor()
135 $mock->expects( $this->any() )
136 ->method( 'getConnectionRef' )
138 ->will( $this->returnValue( $mockDb ) );
144 * @return PHPUnit_Framework_MockObject_MockObject|User
146 private function getMockNonAnonUserWithId( $id ) {
147 $mock = $this->getMockBuilder( User
::class )->getMock();
148 $mock->expects( $this->any() )
150 ->will( $this->returnValue( false ) );
151 $mock->expects( $this->any() )
153 ->will( $this->returnValue( $id ) );
159 * @return PHPUnit_Framework_MockObject_MockObject|User
161 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
162 $mock = $this->getMockNonAnonUserWithId( $id );
163 $mock->expects( $this->any() )
164 ->method( 'isAllowed' )
165 ->will( $this->returnValue( true ) );
166 $mock->expects( $this->any() )
167 ->method( 'isAllowedAny' )
168 ->will( $this->returnValue( true ) );
169 $mock->expects( $this->any() )
170 ->method( 'useRCPatrol' )
171 ->will( $this->returnValue( true ) );
177 * @param string $notAllowedAction
178 * @return PHPUnit_Framework_MockObject_MockObject|User
180 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
181 $mock = $this->getMockNonAnonUserWithId( $id );
183 $mock->expects( $this->any() )
184 ->method( 'isAllowed' )
185 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
186 return $action !== $notAllowedAction;
188 $mock->expects( $this->any() )
189 ->method( 'isAllowedAny' )
190 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
191 $actions = func_get_args();
192 return !in_array( $notAllowedAction, $actions );
200 * @return PHPUnit_Framework_MockObject_MockObject|User
202 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
203 $mock = $this->getMockNonAnonUserWithId( $id );
205 $mock->expects( $this->any() )
206 ->method( 'isAllowed' )
207 ->will( $this->returnValue( true ) );
208 $mock->expects( $this->any() )
209 ->method( 'isAllowedAny' )
210 ->will( $this->returnValue( true ) );
212 $mock->expects( $this->any() )
213 ->method( 'useRCPatrol' )
214 ->will( $this->returnValue( false ) );
215 $mock->expects( $this->any() )
216 ->method( 'useNPPatrol' )
217 ->will( $this->returnValue( false ) );
222 private function getMockAnonUser() {
223 $mock = $this->getMockBuilder( User
::class )->getMock();
224 $mock->expects( $this->any() )
226 ->will( $this->returnValue( true ) );
230 private function getFakeRow( array $rowValues ) {
231 $fakeRow = new stdClass();
232 foreach ( $rowValues as $valueName => $value ) {
233 $fakeRow->$valueName = $value;
238 public function testGetWatchedItemsWithRecentChangeInfo() {
239 $mockDb = $this->getMockDb();
240 $mockDb->expects( $this->once() )
243 [ 'recentchanges', 'watchlist', 'page' ],
251 'wl_notificationtimestamp',
258 '(rc_this_oldid=page_latest) OR (rc_type=3)',
260 $this->isType( 'string' ),
268 'wl_namespace=rc_namespace',
278 ->will( $this->returnValue( [
282 'rc_title' => 'Foo1',
283 'rc_timestamp' => '20151212010101',
286 'wl_notificationtimestamp' => '20151212010101',
291 'rc_title' => 'Foo2',
292 'rc_timestamp' => '20151212010102',
295 'wl_notificationtimestamp' => null,
300 'rc_title' => 'Foo3',
301 'rc_timestamp' => '20151212010103',
304 'wl_notificationtimestamp' => null,
308 $queryService = $this->newService( $mockDb );
309 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
312 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
313 $user, [ 'limit' => 2 ], $startFrom
316 $this->assertInternalType( 'array', $items );
317 $this->assertCount( 2, $items );
319 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
320 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
321 $this->assertInternalType( 'array', $recentChangeInfo );
325 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
332 'rc_title' => 'Foo1',
333 'rc_timestamp' => '20151212010101',
341 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
348 'rc_title' => 'Foo2',
349 'rc_timestamp' => '20151212010102',
356 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
359 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
360 $mockDb = $this->getMockDb();
361 $mockDb->expects( $this->once() )
364 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
372 'wl_notificationtimestamp',
376 'extension_dummy_field',
380 '(rc_this_oldid=page_latest) OR (rc_type=3)',
381 'extension_dummy_cond',
383 $this->isType( 'string' ),
385 'extension_dummy_option',
391 'wl_namespace=rc_namespace',
399 'extension_dummy_join_cond' => [],
402 ->will( $this->returnValue( [
406 'rc_title' => 'Foo1',
407 'rc_timestamp' => '20151212010101',
410 'wl_notificationtimestamp' => '20151212010101',
415 'rc_title' => 'Foo2',
416 'rc_timestamp' => '20151212010102',
419 'wl_notificationtimestamp' => null,
423 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
425 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
427 $mockExtension->expects( $this->once() )
428 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
430 $this->identicalTo( $user ),
431 $this->isType( 'array' ),
432 $this->isInstanceOf( IDatabase
::class ),
433 $this->isType( 'array' ),
434 $this->isType( 'array' ),
435 $this->isType( 'array' ),
436 $this->isType( 'array' ),
437 $this->isType( 'array' )
439 ->will( $this->returnCallback( function (
440 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
442 $tables[] = 'extension_dummy_table';
443 $fields[] = 'extension_dummy_field';
444 $conds[] = 'extension_dummy_cond';
445 $dbOptions[] = 'extension_dummy_option';
446 $joinConds['extension_dummy_join_cond'] = [];
448 $mockExtension->expects( $this->once() )
449 ->method( 'modifyWatchedItemsWithRCInfo' )
451 $this->identicalTo( $user ),
452 $this->isType( 'array' ),
453 $this->isInstanceOf( IDatabase
::class ),
454 $this->isType( 'array' ),
456 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
458 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
459 foreach ( $items as $i => &$item ) {
460 $item[1]['extension_dummy_field'] = $i;
464 $this->assertNull( $startFrom );
465 $startFrom = [ '20160203123456', 42 ];
468 $queryService = $this->newService( $mockDb );
469 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
472 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
473 $user, [], $startFrom
476 $this->assertInternalType( 'array', $items );
477 $this->assertCount( 2, $items );
479 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
480 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
481 $this->assertInternalType( 'array', $recentChangeInfo );
485 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
492 'rc_title' => 'Foo1',
493 'rc_timestamp' => '20151212010101',
496 'extension_dummy_field' => 0,
502 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
509 'rc_title' => 'Foo2',
510 'rc_timestamp' => '20151212010102',
513 'extension_dummy_field' => 1,
518 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
521 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
524 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
527 [ 'rc_type', 'rc_minor', 'rc_bot' ],
533 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
535 [ 'actormigration' => 'table' ],
536 [ 'rc_user_text' => 'actormigration_user_text' ],
539 [ 'actormigration' => 'join' ],
542 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
544 [ 'actormigration' => 'table' ],
545 [ 'rc_user' => 'actormigration_user' ],
548 [ 'actormigration' => 'join' ],
551 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
553 [ 'commentstore' => 'table' ],
554 [ 'commentstore' => 'field' ],
557 [ 'commentstore' => 'join' ],
560 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
563 [ 'rc_patrolled', 'rc_log_type' ],
569 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
572 [ 'rc_old_len', 'rc_new_len' ],
578 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
581 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
587 [ 'namespaceIds' => [ 0, 1 ] ],
591 [ 'wl_namespace' => [ 0, 1 ] ],
596 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
600 [ 'wl_namespace' => [ 0, 1 ] ],
605 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
609 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
614 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
619 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
623 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
628 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
632 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
636 [ "rc_timestamp <= '20151212010101'" ],
637 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
641 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
645 [ "rc_timestamp >= '20151212010101'" ],
646 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
651 'dir' => WatchedItemQueryService
::DIR_OLDER
,
652 'start' => '20151212020101',
653 'end' => '20151212010101'
658 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
659 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
663 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
667 [ "rc_timestamp >= '20151212010101'" ],
668 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
672 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
676 [ "rc_timestamp <= '20151212010101'" ],
677 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
682 'dir' => WatchedItemQueryService
::DIR_NEWER
,
683 'start' => '20151212010101',
684 'end' => '20151212020101'
689 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
690 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
703 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
712 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
721 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
730 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
739 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
748 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
750 [ 'actormigration' => 'table' ],
752 [ 'actormigration is anon' ],
754 [ 'actormigration' => 'join' ],
757 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
759 [ 'actormigration' => 'table' ],
761 [ 'actormigration is not anon' ],
763 [ 'actormigration' => 'join' ],
766 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
770 [ 'rc_patrolled != 0' ],
775 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
779 [ 'rc_patrolled' => 0 ],
784 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
788 [ 'rc_timestamp >= wl_notificationtimestamp' ],
793 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
797 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
802 [ 'onlyByUser' => 'SomeOtherUser' ],
804 [ 'actormigration' => 'table' ],
806 [ 'actormigration_conds' ],
808 [ 'actormigration' => 'join' ],
811 [ 'notByUser' => 'SomeOtherUser' ],
813 [ 'actormigration' => 'table' ],
815 [ 'NOT(actormigration_conds)' ],
817 [ 'actormigration' => 'join' ],
820 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
821 [ '20151212010101', 123 ],
825 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
827 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
831 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
832 [ '20151212010101', 123 ],
836 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
838 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
842 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
843 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
847 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
849 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
856 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
858 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
861 array $expectedExtraTables,
862 array $expectedExtraFields,
863 array $expectedExtraConds,
864 array $expectedDbOptions,
865 array $expectedExtraJoinConds
867 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
868 $expectedFields = array_merge(
876 'wl_notificationtimestamp',
884 $expectedConds = array_merge(
885 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
888 $expectedJoinConds = array_merge(
893 'wl_namespace=rc_namespace',
902 $expectedExtraJoinConds
905 $mockDb = $this->getMockDb();
906 $mockDb->expects( $this->once() )
912 $this->isType( 'string' ),
916 ->will( $this->returnValue( [] ) );
918 $queryService = $this->newService( $mockDb );
919 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
921 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
923 $this->assertEmpty( $items );
924 $this->assertNull( $startFrom );
927 public function filterPatrolledOptionProvider() {
929 [ WatchedItemQueryService
::FILTER_PATROLLED
],
930 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
935 * @dataProvider filterPatrolledOptionProvider
937 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
940 $mockDb = $this->getMockDb();
941 $mockDb->expects( $this->once() )
944 [ 'recentchanges', 'watchlist', 'page' ],
945 $this->isType( 'array' ),
946 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
947 $this->isType( 'string' ),
948 $this->isType( 'array' ),
949 $this->isType( 'array' )
951 ->will( $this->returnValue( [] ) );
953 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
955 $queryService = $this->newService( $mockDb );
956 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
958 [ 'filters' => [ $filtersOption ] ]
961 $this->assertEmpty( $items );
964 public function mysqlIndexOptimizationProvider() {
969 [ "rc_timestamp > ''" ],
973 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
974 [ "rc_timestamp <= '20151212010101'" ],
978 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
979 [ "rc_timestamp >= '20151212010101'" ],
990 * @dataProvider mysqlIndexOptimizationProvider
992 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
995 array $expectedExtraConds
997 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
998 $conds = array_merge( $commonConds, $expectedExtraConds );
1000 $mockDb = $this->getMockDb();
1001 $mockDb->expects( $this->once() )
1002 ->method( 'select' )
1004 [ 'recentchanges', 'watchlist', 'page' ],
1005 $this->isType( 'array' ),
1007 $this->isType( 'string' ),
1008 $this->isType( 'array' ),
1009 $this->isType( 'array' )
1011 ->will( $this->returnValue( [] ) );
1012 $mockDb->expects( $this->any() )
1013 ->method( 'getType' )
1014 ->will( $this->returnValue( $dbType ) );
1016 $queryService = $this->newService( $mockDb );
1017 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1019 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1021 $this->assertEmpty( $items );
1024 public function userPermissionRelatedExtraChecksProvider() {
1031 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1032 LogPage
::DELETED_ACTION
. ')'
1041 '(rc_type != ' . RC_LOG
. ') OR (' .
1042 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1043 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1052 '(rc_type != ' . RC_LOG
. ') OR (' .
1053 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1054 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1059 [ 'onlyByUser' => 'SomeOtherUser' ],
1061 [ 'actormigration' => 'table' ],
1063 'actormigration_conds',
1064 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1065 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1066 LogPage
::DELETED_ACTION
. ')'
1068 [ 'actormigration' => 'join' ],
1071 [ 'onlyByUser' => 'SomeOtherUser' ],
1073 [ 'actormigration' => 'table' ],
1075 'actormigration_conds',
1076 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1077 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1078 '(rc_type != ' . RC_LOG
. ') OR (' .
1079 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1080 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1082 [ 'actormigration' => 'join' ],
1085 [ 'onlyByUser' => 'SomeOtherUser' ],
1087 [ 'actormigration' => 'table' ],
1089 'actormigration_conds',
1090 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1091 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1092 '(rc_type != ' . RC_LOG
. ') OR (' .
1093 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1094 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1096 [ 'actormigration' => 'join' ],
1102 * @dataProvider userPermissionRelatedExtraChecksProvider
1104 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1107 array $expectedExtraTables,
1108 array $expectedExtraConds,
1109 array $expectedExtraJoins
1111 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1112 $conds = array_merge( $commonConds, $expectedExtraConds );
1114 $mockDb = $this->getMockDb();
1115 $mockDb->expects( $this->once() )
1116 ->method( 'select' )
1118 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1119 $this->isType( 'array' ),
1121 $this->isType( 'string' ),
1122 $this->isType( 'array' ),
1124 'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1125 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1126 ], $expectedExtraJoins )
1128 ->will( $this->returnValue( [] ) );
1130 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1132 $queryService = $this->newService( $mockDb );
1133 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1135 $this->assertEmpty( $items );
1138 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1139 $mockDb = $this->getMockDb();
1140 $mockDb->expects( $this->once() )
1141 ->method( 'select' )
1143 [ 'recentchanges', 'watchlist' ],
1151 'wl_notificationtimestamp',
1157 [ 'wl_user' => 1, ],
1158 $this->isType( 'string' ),
1164 'wl_namespace=rc_namespace',
1170 ->will( $this->returnValue( [] ) );
1172 $queryService = $this->newService( $mockDb );
1173 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1175 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1177 $this->assertEmpty( $items );
1180 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1183 [ 'rcTypes' => [ 1337 ] ],
1185 'Bad value for parameter $options[\'rcTypes\']',
1188 [ 'rcTypes' => [ 'edit' ] ],
1190 'Bad value for parameter $options[\'rcTypes\']',
1193 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1195 'Bad value for parameter $options[\'rcTypes\']',
1200 'Bad value for parameter $options[\'dir\']',
1203 [ 'start' => '20151212010101' ],
1205 'Bad value for parameter $options[\'dir\']: must be provided',
1208 [ 'end' => '20151212010101' ],
1210 'Bad value for parameter $options[\'dir\']: must be provided',
1214 [ '20151212010101', 123 ],
1215 'Bad value for parameter $options[\'dir\']: must be provided',
1218 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1220 'Bad value for parameter $startFrom: must be a two-element array',
1223 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1224 [ '20151212010101' ],
1225 'Bad value for parameter $startFrom: must be a two-element array',
1228 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1229 [ '20151212010101', 123, 'foo' ],
1230 'Bad value for parameter $startFrom: must be a two-element array',
1233 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1235 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1238 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1240 'Bad value for parameter $options[\'watchlistOwner\']',
1246 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1248 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1251 $expectedInExceptionMessage
1253 $mockDb = $this->getMockDb();
1254 $mockDb->expects( $this->never() )
1255 ->method( $this->anything() );
1257 $queryService = $this->newService( $mockDb );
1258 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1260 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1261 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1264 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1265 $mockDb = $this->getMockDb();
1266 $mockDb->expects( $this->once() )
1267 ->method( 'select' )
1269 [ 'recentchanges', 'watchlist', 'page' ],
1277 'wl_notificationtimestamp',
1280 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1281 $this->isType( 'string' ),
1287 'wl_namespace=rc_namespace',
1293 'rc_cur_id=page_id',
1297 ->will( $this->returnValue( [] ) );
1299 $queryService = $this->newService( $mockDb );
1300 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1302 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1304 [ 'usedInGenerator' => true ]
1307 $this->assertEmpty( $items );
1310 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1311 $mockDb = $this->getMockDb();
1312 $mockDb->expects( $this->once() )
1313 ->method( 'select' )
1315 [ 'recentchanges', 'watchlist' ],
1323 'wl_notificationtimestamp',
1327 $this->isType( 'string' ),
1333 'wl_namespace=rc_namespace',
1339 ->will( $this->returnValue( [] ) );
1341 $queryService = $this->newService( $mockDb );
1342 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1344 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1346 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1349 $this->assertEmpty( $items );
1352 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1353 $mockDb = $this->getMockDb();
1354 $mockDb->expects( $this->once() )
1355 ->method( 'select' )
1357 $this->isType( 'array' ),
1358 $this->isType( 'array' ),
1361 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1363 $this->isType( 'string' ),
1364 $this->isType( 'array' ),
1365 $this->isType( 'array' )
1367 ->will( $this->returnValue( [] ) );
1369 $queryService = $this->newService( $mockDb );
1370 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1371 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1372 $otherUser->expects( $this->once() )
1373 ->method( 'getOption' )
1374 ->with( 'watchlisttoken' )
1375 ->willReturn( '0123456789abcdef' );
1377 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1379 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1382 $this->assertEmpty( $items );
1385 public function invalidWatchlistTokenProvider() {
1393 * @dataProvider invalidWatchlistTokenProvider
1395 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1396 $mockDb = $this->getMockDb();
1397 $mockDb->expects( $this->never() )
1398 ->method( $this->anything() );
1400 $queryService = $this->newService( $mockDb );
1401 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1402 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1403 $otherUser->expects( $this->once() )
1404 ->method( 'getOption' )
1405 ->with( 'watchlisttoken' )
1406 ->willReturn( '0123456789abcdef' );
1408 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1409 $queryService->getWatchedItemsWithRecentChangeInfo(
1411 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1415 public function testGetWatchedItemsForUser() {
1416 $mockDb = $this->getMockDb();
1417 $mockDb->expects( $this->once() )
1418 ->method( 'select' )
1421 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1424 ->will( $this->returnValue( [
1425 $this->getFakeRow( [
1426 'wl_namespace' => 0,
1427 'wl_title' => 'Foo1',
1428 'wl_notificationtimestamp' => '20151212010101',
1430 $this->getFakeRow( [
1431 'wl_namespace' => 1,
1432 'wl_title' => 'Foo2',
1433 'wl_notificationtimestamp' => null,
1437 $queryService = $this->newService( $mockDb );
1438 $user = $this->getMockNonAnonUserWithId( 1 );
1440 $items = $queryService->getWatchedItemsForUser( $user );
1442 $this->assertInternalType( 'array', $items );
1443 $this->assertCount( 2, $items );
1444 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1445 $this->assertEquals(
1446 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1449 $this->assertEquals(
1450 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1455 public function provideGetWatchedItemsForUserOptions() {
1458 [ 'namespaceIds' => [ 0, 1 ], ],
1459 [ 'wl_namespace' => [ 0, 1 ], ],
1463 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1465 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1469 'namespaceIds' => [ 0 ],
1470 'sort' => WatchedItemQueryService
::SORT_ASC
,
1472 [ 'wl_namespace' => [ 0 ], ],
1473 [ 'ORDER BY' => 'wl_title ASC' ]
1482 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1483 'limit' => "10; DROP TABLE watchlist;\n--",
1485 [ 'wl_namespace' => [ 0, 1 ], ],
1489 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1490 [ 'wl_notificationtimestamp IS NOT NULL' ],
1494 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1495 [ 'wl_notificationtimestamp IS NULL' ],
1499 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1501 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1505 'namespaceIds' => [ 0 ],
1506 'sort' => WatchedItemQueryService
::SORT_DESC
,
1508 [ 'wl_namespace' => [ 0 ], ],
1509 [ 'ORDER BY' => 'wl_title DESC' ]
1515 * @dataProvider provideGetWatchedItemsForUserOptions
1517 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1519 array $expectedConds,
1520 array $expectedDbOptions
1522 $mockDb = $this->getMockDb();
1523 $user = $this->getMockNonAnonUserWithId( 1 );
1525 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1526 $mockDb->expects( $this->once() )
1527 ->method( 'select' )
1530 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1532 $this->isType( 'string' ),
1535 ->will( $this->returnValue( [] ) );
1537 $queryService = $this->newService( $mockDb );
1539 $items = $queryService->getWatchedItemsForUser( $user, $options );
1540 $this->assertEmpty( $items );
1543 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1547 'from' => new TitleValue( 0, 'SomeDbKey' ),
1548 'sort' => WatchedItemQueryService
::SORT_ASC
1550 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1551 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1555 'from' => new TitleValue( 0, 'SomeDbKey' ),
1556 'sort' => WatchedItemQueryService
::SORT_DESC
,
1558 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1559 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1563 'until' => new TitleValue( 0, 'SomeDbKey' ),
1564 'sort' => WatchedItemQueryService
::SORT_ASC
1566 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1567 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1571 'until' => new TitleValue( 0, 'SomeDbKey' ),
1572 'sort' => WatchedItemQueryService
::SORT_DESC
1574 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1575 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1579 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1580 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1581 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1582 'sort' => WatchedItemQueryService
::SORT_ASC
1585 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1586 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1587 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1589 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1593 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1594 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1595 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1596 'sort' => WatchedItemQueryService
::SORT_DESC
1599 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1600 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1601 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1603 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1609 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1611 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1613 array $expectedConds,
1614 array $expectedDbOptions
1616 $user = $this->getMockNonAnonUserWithId( 1 );
1618 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1620 $mockDb = $this->getMockDb();
1621 $mockDb->expects( $this->any() )
1622 ->method( 'addQuotes' )
1623 ->will( $this->returnCallback( function ( $value ) {
1626 $mockDb->expects( $this->any() )
1627 ->method( 'makeList' )
1629 $this->isType( 'array' ),
1630 $this->isType( 'int' )
1632 ->will( $this->returnCallback( function ( $a, $conj ) {
1633 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1634 return implode( $sqlConj, array_map( function ( $s ) {
1635 return '(' . $s . ')';
1639 $mockDb->expects( $this->once() )
1640 ->method( 'select' )
1643 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1645 $this->isType( 'string' ),
1648 ->will( $this->returnValue( [] ) );
1650 $queryService = $this->newService( $mockDb );
1652 $items = $queryService->getWatchedItemsForUser( $user, $options );
1653 $this->assertEmpty( $items );
1656 public function getWatchedItemsForUserInvalidOptionsProvider() {
1659 [ 'sort' => 'foo' ],
1660 'Bad value for parameter $options[\'sort\']'
1663 [ 'filter' => 'foo' ],
1664 'Bad value for parameter $options[\'filter\']'
1667 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1668 'Bad value for parameter $options[\'sort\']: must be provided'
1671 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1672 'Bad value for parameter $options[\'sort\']: must be provided'
1675 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1676 'Bad value for parameter $options[\'sort\']: must be provided'
1682 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1684 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1686 $expectedInExceptionMessage
1688 $queryService = $this->newService( $this->getMockDb() );
1690 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1691 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1694 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1695 $mockDb = $this->getMockDb();
1697 $mockDb->expects( $this->never() )
1698 ->method( $this->anything() );
1700 $queryService = $this->newService( $mockDb );
1702 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1703 $this->assertEmpty( $items );