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 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
35 * @return WatchedItemQueryService
37 private function newService( $mockDb ) {
38 return new WatchedItemQueryService(
39 $this->getMockLoadBalancer( $mockDb ),
40 $this->getMockCommentStore()
45 * @return PHPUnit_Framework_MockObject_MockObject|Database
47 private function getMockDb() {
48 $mock = $this->getMockBuilder( Database
::class )
49 ->disableOriginalConstructor()
52 $mock->expects( $this->any() )
53 ->method( 'makeList' )
55 $this->isType( 'array' ),
56 $this->isType( 'int' )
58 ->will( $this->returnCallback( function ( $a, $conj ) {
59 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
60 return join( $sqlConj, array_map( function ( $s ) {
61 return '(' . $s . ')';
66 $mock->expects( $this->any() )
67 ->method( 'addQuotes' )
68 ->will( $this->returnCallback( function ( $value ) {
72 $mock->expects( $this->any() )
73 ->method( 'timestamp' )
74 ->will( $this->returnArgument( 0 ) );
76 $mock->expects( $this->any() )
78 ->willReturnCallback( function ( $a, $b ) {
86 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
87 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
89 private function getMockLoadBalancer( $mockDb ) {
90 $mock = $this->getMockBuilder( LoadBalancer
::class )
91 ->disableOriginalConstructor()
93 $mock->expects( $this->any() )
94 ->method( 'getConnectionRef' )
96 ->will( $this->returnValue( $mockDb ) );
102 * @return PHPUnit_Framework_MockObject_MockObject|User
104 private function getMockNonAnonUserWithId( $id ) {
105 $mock = $this->getMockBuilder( User
::class )->getMock();
106 $mock->expects( $this->any() )
108 ->will( $this->returnValue( false ) );
109 $mock->expects( $this->any() )
111 ->will( $this->returnValue( $id ) );
117 * @return PHPUnit_Framework_MockObject_MockObject|User
119 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
120 $mock = $this->getMockNonAnonUserWithId( $id );
121 $mock->expects( $this->any() )
122 ->method( 'isAllowed' )
123 ->will( $this->returnValue( true ) );
124 $mock->expects( $this->any() )
125 ->method( 'isAllowedAny' )
126 ->will( $this->returnValue( true ) );
127 $mock->expects( $this->any() )
128 ->method( 'useRCPatrol' )
129 ->will( $this->returnValue( true ) );
135 * @param string $notAllowedAction
136 * @return PHPUnit_Framework_MockObject_MockObject|User
138 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
139 $mock = $this->getMockNonAnonUserWithId( $id );
141 $mock->expects( $this->any() )
142 ->method( 'isAllowed' )
143 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
144 return $action !== $notAllowedAction;
146 $mock->expects( $this->any() )
147 ->method( 'isAllowedAny' )
148 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
149 $actions = func_get_args();
150 return !in_array( $notAllowedAction, $actions );
158 * @return PHPUnit_Framework_MockObject_MockObject|User
160 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
161 $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 ) );
170 $mock->expects( $this->any() )
171 ->method( 'useRCPatrol' )
172 ->will( $this->returnValue( false ) );
173 $mock->expects( $this->any() )
174 ->method( 'useNPPatrol' )
175 ->will( $this->returnValue( false ) );
180 private function getMockAnonUser() {
181 $mock = $this->getMockBuilder( User
::class )->getMock();
182 $mock->expects( $this->any() )
184 ->will( $this->returnValue( true ) );
188 private function getFakeRow( array $rowValues ) {
189 $fakeRow = new stdClass();
190 foreach ( $rowValues as $valueName => $value ) {
191 $fakeRow->$valueName = $value;
196 public function testGetWatchedItemsWithRecentChangeInfo() {
197 $mockDb = $this->getMockDb();
198 $mockDb->expects( $this->once() )
201 [ 'recentchanges', 'watchlist', 'page' ],
209 'wl_notificationtimestamp',
216 '(rc_this_oldid=page_latest) OR (rc_type=3)',
218 $this->isType( 'string' ),
226 'wl_namespace=rc_namespace',
236 ->will( $this->returnValue( [
240 'rc_title' => 'Foo1',
241 'rc_timestamp' => '20151212010101',
244 'wl_notificationtimestamp' => '20151212010101',
249 'rc_title' => 'Foo2',
250 'rc_timestamp' => '20151212010102',
253 'wl_notificationtimestamp' => null,
258 'rc_title' => 'Foo3',
259 'rc_timestamp' => '20151212010103',
262 'wl_notificationtimestamp' => null,
266 $queryService = $this->newService( $mockDb );
267 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
270 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
271 $user, [ 'limit' => 2 ], $startFrom
274 $this->assertInternalType( 'array', $items );
275 $this->assertCount( 2, $items );
277 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
278 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
279 $this->assertInternalType( 'array', $recentChangeInfo );
283 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
290 'rc_title' => 'Foo1',
291 'rc_timestamp' => '20151212010101',
299 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
306 'rc_title' => 'Foo2',
307 'rc_timestamp' => '20151212010102',
314 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
317 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
318 $mockDb = $this->getMockDb();
319 $mockDb->expects( $this->once() )
322 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
330 'wl_notificationtimestamp',
334 'extension_dummy_field',
338 '(rc_this_oldid=page_latest) OR (rc_type=3)',
339 'extension_dummy_cond',
341 $this->isType( 'string' ),
343 'extension_dummy_option',
349 'wl_namespace=rc_namespace',
357 'extension_dummy_join_cond' => [],
360 ->will( $this->returnValue( [
364 'rc_title' => 'Foo1',
365 'rc_timestamp' => '20151212010101',
368 'wl_notificationtimestamp' => '20151212010101',
373 'rc_title' => 'Foo2',
374 'rc_timestamp' => '20151212010102',
377 'wl_notificationtimestamp' => null,
381 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
383 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
385 $mockExtension->expects( $this->once() )
386 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
388 $this->identicalTo( $user ),
389 $this->isType( 'array' ),
390 $this->isInstanceOf( IDatabase
::class ),
391 $this->isType( 'array' ),
392 $this->isType( 'array' ),
393 $this->isType( 'array' ),
394 $this->isType( 'array' ),
395 $this->isType( 'array' )
397 ->will( $this->returnCallback( function (
398 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
400 $tables[] = 'extension_dummy_table';
401 $fields[] = 'extension_dummy_field';
402 $conds[] = 'extension_dummy_cond';
403 $dbOptions[] = 'extension_dummy_option';
404 $joinConds['extension_dummy_join_cond'] = [];
406 $mockExtension->expects( $this->once() )
407 ->method( 'modifyWatchedItemsWithRCInfo' )
409 $this->identicalTo( $user ),
410 $this->isType( 'array' ),
411 $this->isInstanceOf( IDatabase
::class ),
412 $this->isType( 'array' ),
414 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
416 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
417 foreach ( $items as $i => &$item ) {
418 $item[1]['extension_dummy_field'] = $i;
422 $this->assertNull( $startFrom );
423 $startFrom = [ '20160203123456', 42 ];
426 $queryService = $this->newService( $mockDb );
427 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
430 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
431 $user, [], $startFrom
434 $this->assertInternalType( 'array', $items );
435 $this->assertCount( 2, $items );
437 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
438 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
439 $this->assertInternalType( 'array', $recentChangeInfo );
443 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
450 'rc_title' => 'Foo1',
451 'rc_timestamp' => '20151212010101',
454 'extension_dummy_field' => 0,
460 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
467 'rc_title' => 'Foo2',
468 'rc_timestamp' => '20151212010102',
471 'extension_dummy_field' => 1,
476 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
479 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
482 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
485 [ 'rc_type', 'rc_minor', 'rc_bot' ],
491 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
500 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
509 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
511 [ 'commentstore' => 'table' ],
512 [ 'commentstore' => 'field' ],
515 [ 'commentstore' => 'join' ],
518 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
521 [ 'rc_patrolled', 'rc_log_type' ],
527 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
530 [ 'rc_old_len', 'rc_new_len' ],
536 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
539 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
545 [ 'namespaceIds' => [ 0, 1 ] ],
549 [ 'wl_namespace' => [ 0, 1 ] ],
554 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
558 [ 'wl_namespace' => [ 0, 1 ] ],
563 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
567 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
572 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
577 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
581 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
586 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
590 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
594 [ "rc_timestamp <= '20151212010101'" ],
595 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
599 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
603 [ "rc_timestamp >= '20151212010101'" ],
604 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
609 'dir' => WatchedItemQueryService
::DIR_OLDER
,
610 'start' => '20151212020101',
611 'end' => '20151212010101'
616 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
617 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
621 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
625 [ "rc_timestamp >= '20151212010101'" ],
626 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
630 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
634 [ "rc_timestamp <= '20151212010101'" ],
635 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
640 'dir' => WatchedItemQueryService
::DIR_NEWER
,
641 'start' => '20151212010101',
642 'end' => '20151212020101'
647 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
648 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
661 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
670 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
679 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
688 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
697 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
706 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
715 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
724 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
728 [ 'rc_patrolled != 0' ],
733 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
737 [ 'rc_patrolled = 0' ],
742 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
746 [ 'rc_timestamp >= wl_notificationtimestamp' ],
751 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
755 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
760 [ 'onlyByUser' => 'SomeOtherUser' ],
764 [ 'rc_user_text' => 'SomeOtherUser' ],
769 [ 'notByUser' => 'SomeOtherUser' ],
773 [ "rc_user_text != 'SomeOtherUser'" ],
778 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
779 [ '20151212010101', 123 ],
783 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
785 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
789 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
790 [ '20151212010101', 123 ],
794 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
796 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
800 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
801 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
805 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
807 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
814 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
816 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
819 array $expectedExtraTables,
820 array $expectedExtraFields,
821 array $expectedExtraConds,
822 array $expectedDbOptions,
823 array $expectedExtraJoinConds
825 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
826 $expectedFields = array_merge(
834 'wl_notificationtimestamp',
842 $expectedConds = array_merge(
843 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
846 $expectedJoinConds = array_merge(
851 'wl_namespace=rc_namespace',
860 $expectedExtraJoinConds
863 $mockDb = $this->getMockDb();
864 $mockDb->expects( $this->once() )
870 $this->isType( 'string' ),
874 ->will( $this->returnValue( [] ) );
876 $queryService = $this->newService( $mockDb );
877 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
879 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
881 $this->assertEmpty( $items );
882 $this->assertNull( $startFrom );
885 public function filterPatrolledOptionProvider() {
887 [ WatchedItemQueryService
::FILTER_PATROLLED
],
888 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
893 * @dataProvider filterPatrolledOptionProvider
895 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
898 $mockDb = $this->getMockDb();
899 $mockDb->expects( $this->once() )
902 [ 'recentchanges', 'watchlist', 'page' ],
903 $this->isType( 'array' ),
904 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
905 $this->isType( 'string' ),
906 $this->isType( 'array' ),
907 $this->isType( 'array' )
909 ->will( $this->returnValue( [] ) );
911 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
913 $queryService = $this->newService( $mockDb );
914 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
916 [ 'filters' => [ $filtersOption ] ]
919 $this->assertEmpty( $items );
922 public function mysqlIndexOptimizationProvider() {
927 [ "rc_timestamp > ''" ],
931 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
932 [ "rc_timestamp <= '20151212010101'" ],
936 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
937 [ "rc_timestamp >= '20151212010101'" ],
948 * @dataProvider mysqlIndexOptimizationProvider
950 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
953 array $expectedExtraConds
955 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
956 $conds = array_merge( $commonConds, $expectedExtraConds );
958 $mockDb = $this->getMockDb();
959 $mockDb->expects( $this->once() )
962 [ 'recentchanges', 'watchlist', 'page' ],
963 $this->isType( 'array' ),
965 $this->isType( 'string' ),
966 $this->isType( 'array' ),
967 $this->isType( 'array' )
969 ->will( $this->returnValue( [] ) );
970 $mockDb->expects( $this->any() )
971 ->method( 'getType' )
972 ->will( $this->returnValue( $dbType ) );
974 $queryService = $this->newService( $mockDb );
975 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
977 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
979 $this->assertEmpty( $items );
982 public function userPermissionRelatedExtraChecksProvider() {
988 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
989 LogPage
::DELETED_ACTION
. ')'
996 '(rc_type != ' . RC_LOG
. ') OR (' .
997 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
998 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1005 '(rc_type != ' . RC_LOG
. ') OR (' .
1006 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1007 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1011 [ 'onlyByUser' => 'SomeOtherUser' ],
1014 'rc_user_text' => 'SomeOtherUser',
1015 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1016 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1017 LogPage
::DELETED_ACTION
. ')'
1021 [ 'onlyByUser' => 'SomeOtherUser' ],
1024 'rc_user_text' => 'SomeOtherUser',
1025 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1026 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1027 '(rc_type != ' . RC_LOG
. ') OR (' .
1028 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1029 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1033 [ 'onlyByUser' => 'SomeOtherUser' ],
1036 'rc_user_text' => 'SomeOtherUser',
1037 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1038 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1039 '(rc_type != ' . RC_LOG
. ') OR (' .
1040 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1041 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1048 * @dataProvider userPermissionRelatedExtraChecksProvider
1050 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1053 array $expectedExtraConds
1055 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1056 $conds = array_merge( $commonConds, $expectedExtraConds );
1058 $mockDb = $this->getMockDb();
1059 $mockDb->expects( $this->once() )
1060 ->method( 'select' )
1062 [ 'recentchanges', 'watchlist', 'page' ],
1063 $this->isType( 'array' ),
1065 $this->isType( 'string' ),
1066 $this->isType( 'array' ),
1067 $this->isType( 'array' )
1069 ->will( $this->returnValue( [] ) );
1071 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1073 $queryService = $this->newService( $mockDb );
1074 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1076 $this->assertEmpty( $items );
1079 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1080 $mockDb = $this->getMockDb();
1081 $mockDb->expects( $this->once() )
1082 ->method( 'select' )
1084 [ 'recentchanges', 'watchlist' ],
1092 'wl_notificationtimestamp',
1098 [ 'wl_user' => 1, ],
1099 $this->isType( 'string' ),
1105 'wl_namespace=rc_namespace',
1111 ->will( $this->returnValue( [] ) );
1113 $queryService = $this->newService( $mockDb );
1114 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1116 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1118 $this->assertEmpty( $items );
1121 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1124 [ 'rcTypes' => [ 1337 ] ],
1126 'Bad value for parameter $options[\'rcTypes\']',
1129 [ 'rcTypes' => [ 'edit' ] ],
1131 'Bad value for parameter $options[\'rcTypes\']',
1134 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1136 'Bad value for parameter $options[\'rcTypes\']',
1141 'Bad value for parameter $options[\'dir\']',
1144 [ 'start' => '20151212010101' ],
1146 'Bad value for parameter $options[\'dir\']: must be provided',
1149 [ 'end' => '20151212010101' ],
1151 'Bad value for parameter $options[\'dir\']: must be provided',
1155 [ '20151212010101', 123 ],
1156 'Bad value for parameter $options[\'dir\']: must be provided',
1159 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1161 'Bad value for parameter $startFrom: must be a two-element array',
1164 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1165 [ '20151212010101' ],
1166 'Bad value for parameter $startFrom: must be a two-element array',
1169 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1170 [ '20151212010101', 123, 'foo' ],
1171 'Bad value for parameter $startFrom: must be a two-element array',
1174 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1176 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1179 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1181 'Bad value for parameter $options[\'watchlistOwner\']',
1187 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1189 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1192 $expectedInExceptionMessage
1194 $mockDb = $this->getMockDb();
1195 $mockDb->expects( $this->never() )
1196 ->method( $this->anything() );
1198 $queryService = $this->newService( $mockDb );
1199 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1201 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1202 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1205 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1206 $mockDb = $this->getMockDb();
1207 $mockDb->expects( $this->once() )
1208 ->method( 'select' )
1210 [ 'recentchanges', 'watchlist', 'page' ],
1218 'wl_notificationtimestamp',
1221 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1222 $this->isType( 'string' ),
1228 'wl_namespace=rc_namespace',
1234 'rc_cur_id=page_id',
1238 ->will( $this->returnValue( [] ) );
1240 $queryService = $this->newService( $mockDb );
1241 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1243 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1245 [ 'usedInGenerator' => true ]
1248 $this->assertEmpty( $items );
1251 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1252 $mockDb = $this->getMockDb();
1253 $mockDb->expects( $this->once() )
1254 ->method( 'select' )
1256 [ 'recentchanges', 'watchlist' ],
1264 'wl_notificationtimestamp',
1268 $this->isType( 'string' ),
1274 'wl_namespace=rc_namespace',
1280 ->will( $this->returnValue( [] ) );
1282 $queryService = $this->newService( $mockDb );
1283 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1285 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1287 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1290 $this->assertEmpty( $items );
1293 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1294 $mockDb = $this->getMockDb();
1295 $mockDb->expects( $this->once() )
1296 ->method( 'select' )
1298 $this->isType( 'array' ),
1299 $this->isType( 'array' ),
1302 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1304 $this->isType( 'string' ),
1305 $this->isType( 'array' ),
1306 $this->isType( 'array' )
1308 ->will( $this->returnValue( [] ) );
1310 $queryService = $this->newService( $mockDb );
1311 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1312 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1313 $otherUser->expects( $this->once() )
1314 ->method( 'getOption' )
1315 ->with( 'watchlisttoken' )
1316 ->willReturn( '0123456789abcdef' );
1318 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1320 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1323 $this->assertEmpty( $items );
1326 public function invalidWatchlistTokenProvider() {
1334 * @dataProvider invalidWatchlistTokenProvider
1336 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1337 $mockDb = $this->getMockDb();
1338 $mockDb->expects( $this->never() )
1339 ->method( $this->anything() );
1341 $queryService = $this->newService( $mockDb );
1342 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1343 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1344 $otherUser->expects( $this->once() )
1345 ->method( 'getOption' )
1346 ->with( 'watchlisttoken' )
1347 ->willReturn( '0123456789abcdef' );
1349 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1350 $queryService->getWatchedItemsWithRecentChangeInfo(
1352 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1356 public function testGetWatchedItemsForUser() {
1357 $mockDb = $this->getMockDb();
1358 $mockDb->expects( $this->once() )
1359 ->method( 'select' )
1362 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1365 ->will( $this->returnValue( [
1366 $this->getFakeRow( [
1367 'wl_namespace' => 0,
1368 'wl_title' => 'Foo1',
1369 'wl_notificationtimestamp' => '20151212010101',
1371 $this->getFakeRow( [
1372 'wl_namespace' => 1,
1373 'wl_title' => 'Foo2',
1374 'wl_notificationtimestamp' => null,
1378 $queryService = $this->newService( $mockDb );
1379 $user = $this->getMockNonAnonUserWithId( 1 );
1381 $items = $queryService->getWatchedItemsForUser( $user );
1383 $this->assertInternalType( 'array', $items );
1384 $this->assertCount( 2, $items );
1385 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1386 $this->assertEquals(
1387 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1390 $this->assertEquals(
1391 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1396 public function provideGetWatchedItemsForUserOptions() {
1399 [ 'namespaceIds' => [ 0, 1 ], ],
1400 [ 'wl_namespace' => [ 0, 1 ], ],
1404 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1406 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1410 'namespaceIds' => [ 0 ],
1411 'sort' => WatchedItemQueryService
::SORT_ASC
,
1413 [ 'wl_namespace' => [ 0 ], ],
1414 [ 'ORDER BY' => 'wl_title ASC' ]
1423 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1424 'limit' => "10; DROP TABLE watchlist;\n--",
1426 [ 'wl_namespace' => [ 0, 1 ], ],
1430 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1431 [ 'wl_notificationtimestamp IS NOT NULL' ],
1435 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1436 [ 'wl_notificationtimestamp IS NULL' ],
1440 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1442 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1446 'namespaceIds' => [ 0 ],
1447 'sort' => WatchedItemQueryService
::SORT_DESC
,
1449 [ 'wl_namespace' => [ 0 ], ],
1450 [ 'ORDER BY' => 'wl_title DESC' ]
1456 * @dataProvider provideGetWatchedItemsForUserOptions
1458 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1460 array $expectedConds,
1461 array $expectedDbOptions
1463 $mockDb = $this->getMockDb();
1464 $user = $this->getMockNonAnonUserWithId( 1 );
1466 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1467 $mockDb->expects( $this->once() )
1468 ->method( 'select' )
1471 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1473 $this->isType( 'string' ),
1476 ->will( $this->returnValue( [] ) );
1478 $queryService = $this->newService( $mockDb );
1480 $items = $queryService->getWatchedItemsForUser( $user, $options );
1481 $this->assertEmpty( $items );
1484 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1488 'from' => new TitleValue( 0, 'SomeDbKey' ),
1489 'sort' => WatchedItemQueryService
::SORT_ASC
1491 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1492 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1496 'from' => new TitleValue( 0, 'SomeDbKey' ),
1497 'sort' => WatchedItemQueryService
::SORT_DESC
,
1499 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1500 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1504 'until' => new TitleValue( 0, 'SomeDbKey' ),
1505 'sort' => WatchedItemQueryService
::SORT_ASC
1507 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1508 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1512 'until' => new TitleValue( 0, 'SomeDbKey' ),
1513 'sort' => WatchedItemQueryService
::SORT_DESC
1515 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1516 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1520 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1521 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1522 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1523 'sort' => WatchedItemQueryService
::SORT_ASC
1526 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1527 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1528 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1530 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1534 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1535 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1536 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1537 'sort' => WatchedItemQueryService
::SORT_DESC
1540 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1541 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1542 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1544 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1550 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1552 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1554 array $expectedConds,
1555 array $expectedDbOptions
1557 $user = $this->getMockNonAnonUserWithId( 1 );
1559 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1561 $mockDb = $this->getMockDb();
1562 $mockDb->expects( $this->any() )
1563 ->method( 'addQuotes' )
1564 ->will( $this->returnCallback( function ( $value ) {
1567 $mockDb->expects( $this->any() )
1568 ->method( 'makeList' )
1570 $this->isType( 'array' ),
1571 $this->isType( 'int' )
1573 ->will( $this->returnCallback( function ( $a, $conj ) {
1574 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1575 return join( $sqlConj, array_map( function ( $s ) {
1576 return '(' . $s . ')';
1580 $mockDb->expects( $this->once() )
1581 ->method( 'select' )
1584 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1586 $this->isType( 'string' ),
1589 ->will( $this->returnValue( [] ) );
1591 $queryService = $this->newService( $mockDb );
1593 $items = $queryService->getWatchedItemsForUser( $user, $options );
1594 $this->assertEmpty( $items );
1597 public function getWatchedItemsForUserInvalidOptionsProvider() {
1600 [ 'sort' => 'foo' ],
1601 'Bad value for parameter $options[\'sort\']'
1604 [ 'filter' => 'foo' ],
1605 'Bad value for parameter $options[\'filter\']'
1608 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1609 'Bad value for parameter $options[\'sort\']: must be provided'
1612 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1613 'Bad value for parameter $options[\'sort\']: must be provided'
1616 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1617 'Bad value for parameter $options[\'sort\']: must be provided'
1623 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1625 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1627 $expectedInExceptionMessage
1629 $queryService = $this->newService( $this->getMockDb() );
1631 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1632 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1635 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1636 $mockDb = $this->getMockDb();
1638 $mockDb->expects( $this->never() )
1639 ->method( $this->anything() );
1641 $queryService = $this->newService( $mockDb );
1643 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1644 $this->assertEmpty( $items );