3 use Wikimedia\TestingAccessWrapper
;
6 * @covers WatchedItemQueryService
8 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
10 use MediaWikiCoversValidator
;
12 private function overrideCommentStore() {
13 $mockStore = $this->getMockBuilder( CommentStore
::class )
14 ->disableOriginalConstructor()
16 $mockStore->expects( $this->any() )
17 ->method( 'getFields' )
18 ->willReturn( [ 'commentstore' => 'fields' ] );
19 $mockStore->expects( $this->any() )
22 'tables' => [ 'commentstore' => 'table' ],
23 'fields' => [ 'commentstore' => 'field' ],
24 'joins' => [ 'commentstore' => 'join' ],
27 $this->setService( 'CommentStore', $mockStore );
31 * @return PHPUnit_Framework_MockObject_MockObject|Database
33 private function getMockDb() {
34 $mock = $this->getMockBuilder( Database
::class )
35 ->disableOriginalConstructor()
38 $mock->expects( $this->any() )
39 ->method( 'makeList' )
41 $this->isType( 'array' ),
42 $this->isType( 'int' )
44 ->will( $this->returnCallback( function ( $a, $conj ) {
45 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
46 return join( $sqlConj, array_map( function ( $s ) {
47 return '(' . $s . ')';
52 $mock->expects( $this->any() )
53 ->method( 'addQuotes' )
54 ->will( $this->returnCallback( function ( $value ) {
58 $mock->expects( $this->any() )
59 ->method( 'timestamp' )
60 ->will( $this->returnArgument( 0 ) );
62 $mock->expects( $this->any() )
64 ->willReturnCallback( function ( $a, $b ) {
72 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
73 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
75 private function getMockLoadBalancer( $mockDb ) {
76 $mock = $this->getMockBuilder( LoadBalancer
::class )
77 ->disableOriginalConstructor()
79 $mock->expects( $this->any() )
80 ->method( 'getConnectionRef' )
82 ->will( $this->returnValue( $mockDb ) );
88 * @return PHPUnit_Framework_MockObject_MockObject|User
90 private function getMockNonAnonUserWithId( $id ) {
91 $mock = $this->getMockBuilder( User
::class )->getMock();
92 $mock->expects( $this->any() )
94 ->will( $this->returnValue( false ) );
95 $mock->expects( $this->any() )
97 ->will( $this->returnValue( $id ) );
103 * @return PHPUnit_Framework_MockObject_MockObject|User
105 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
106 $mock = $this->getMockNonAnonUserWithId( $id );
107 $mock->expects( $this->any() )
108 ->method( 'isAllowed' )
109 ->will( $this->returnValue( true ) );
110 $mock->expects( $this->any() )
111 ->method( 'isAllowedAny' )
112 ->will( $this->returnValue( true ) );
113 $mock->expects( $this->any() )
114 ->method( 'useRCPatrol' )
115 ->will( $this->returnValue( true ) );
121 * @param string $notAllowedAction
122 * @return PHPUnit_Framework_MockObject_MockObject|User
124 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
125 $mock = $this->getMockNonAnonUserWithId( $id );
127 $mock->expects( $this->any() )
128 ->method( 'isAllowed' )
129 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
130 return $action !== $notAllowedAction;
132 $mock->expects( $this->any() )
133 ->method( 'isAllowedAny' )
134 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
135 $actions = func_get_args();
136 return !in_array( $notAllowedAction, $actions );
144 * @return PHPUnit_Framework_MockObject_MockObject|User
146 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
147 $mock = $this->getMockNonAnonUserWithId( $id );
149 $mock->expects( $this->any() )
150 ->method( 'isAllowed' )
151 ->will( $this->returnValue( true ) );
152 $mock->expects( $this->any() )
153 ->method( 'isAllowedAny' )
154 ->will( $this->returnValue( true ) );
156 $mock->expects( $this->any() )
157 ->method( 'useRCPatrol' )
158 ->will( $this->returnValue( false ) );
159 $mock->expects( $this->any() )
160 ->method( 'useNPPatrol' )
161 ->will( $this->returnValue( false ) );
166 private function getMockAnonUser() {
167 $mock = $this->getMockBuilder( User
::class )->getMock();
168 $mock->expects( $this->any() )
170 ->will( $this->returnValue( true ) );
174 private function getFakeRow( array $rowValues ) {
175 $fakeRow = new stdClass();
176 foreach ( $rowValues as $valueName => $value ) {
177 $fakeRow->$valueName = $value;
182 public function testGetWatchedItemsWithRecentChangeInfo() {
183 $mockDb = $this->getMockDb();
184 $mockDb->expects( $this->once() )
187 [ 'recentchanges', 'watchlist', 'page' ],
195 'wl_notificationtimestamp',
202 '(rc_this_oldid=page_latest) OR (rc_type=3)',
204 $this->isType( 'string' ),
212 'wl_namespace=rc_namespace',
222 ->will( $this->returnValue( [
226 'rc_title' => 'Foo1',
227 'rc_timestamp' => '20151212010101',
230 'wl_notificationtimestamp' => '20151212010101',
235 'rc_title' => 'Foo2',
236 'rc_timestamp' => '20151212010102',
239 'wl_notificationtimestamp' => null,
244 'rc_title' => 'Foo3',
245 'rc_timestamp' => '20151212010103',
248 'wl_notificationtimestamp' => null,
252 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
253 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
256 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
257 $user, [ 'limit' => 2 ], $startFrom
260 $this->assertInternalType( 'array', $items );
261 $this->assertCount( 2, $items );
263 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
264 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
265 $this->assertInternalType( 'array', $recentChangeInfo );
269 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
276 'rc_title' => 'Foo1',
277 'rc_timestamp' => '20151212010101',
285 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
292 'rc_title' => 'Foo2',
293 'rc_timestamp' => '20151212010102',
300 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
303 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
304 $mockDb = $this->getMockDb();
305 $mockDb->expects( $this->once() )
308 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
316 'wl_notificationtimestamp',
320 'extension_dummy_field',
324 '(rc_this_oldid=page_latest) OR (rc_type=3)',
325 'extension_dummy_cond',
327 $this->isType( 'string' ),
329 'extension_dummy_option',
335 'wl_namespace=rc_namespace',
343 'extension_dummy_join_cond' => [],
346 ->will( $this->returnValue( [
350 'rc_title' => 'Foo1',
351 'rc_timestamp' => '20151212010101',
354 'wl_notificationtimestamp' => '20151212010101',
359 'rc_title' => 'Foo2',
360 'rc_timestamp' => '20151212010102',
363 'wl_notificationtimestamp' => null,
367 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
369 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
371 $mockExtension->expects( $this->once() )
372 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
374 $this->identicalTo( $user ),
375 $this->isType( 'array' ),
376 $this->isInstanceOf( IDatabase
::class ),
377 $this->isType( 'array' ),
378 $this->isType( 'array' ),
379 $this->isType( 'array' ),
380 $this->isType( 'array' ),
381 $this->isType( 'array' )
383 ->will( $this->returnCallback( function (
384 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
386 $tables[] = 'extension_dummy_table';
387 $fields[] = 'extension_dummy_field';
388 $conds[] = 'extension_dummy_cond';
389 $dbOptions[] = 'extension_dummy_option';
390 $joinConds['extension_dummy_join_cond'] = [];
392 $mockExtension->expects( $this->once() )
393 ->method( 'modifyWatchedItemsWithRCInfo' )
395 $this->identicalTo( $user ),
396 $this->isType( 'array' ),
397 $this->isInstanceOf( IDatabase
::class ),
398 $this->isType( 'array' ),
400 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
402 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
403 foreach ( $items as $i => &$item ) {
404 $item[1]['extension_dummy_field'] = $i;
408 $this->assertNull( $startFrom );
409 $startFrom = [ '20160203123456', 42 ];
412 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
413 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
416 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
417 $user, [], $startFrom
420 $this->assertInternalType( 'array', $items );
421 $this->assertCount( 2, $items );
423 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
424 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
425 $this->assertInternalType( 'array', $recentChangeInfo );
429 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
436 'rc_title' => 'Foo1',
437 'rc_timestamp' => '20151212010101',
440 'extension_dummy_field' => 0,
446 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
453 'rc_title' => 'Foo2',
454 'rc_timestamp' => '20151212010102',
457 'extension_dummy_field' => 1,
462 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
465 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
468 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
471 [ 'rc_type', 'rc_minor', 'rc_bot' ],
477 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
486 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
495 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
497 [ 'commentstore' => 'table' ],
498 [ 'commentstore' => 'field' ],
501 [ 'commentstore' => 'join' ],
504 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
507 [ 'rc_patrolled', 'rc_log_type' ],
513 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
516 [ 'rc_old_len', 'rc_new_len' ],
522 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
525 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
531 [ 'namespaceIds' => [ 0, 1 ] ],
535 [ 'wl_namespace' => [ 0, 1 ] ],
540 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
544 [ 'wl_namespace' => [ 0, 1 ] ],
549 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
553 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
558 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
563 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
567 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
572 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
576 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
580 [ "rc_timestamp <= '20151212010101'" ],
581 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
585 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
589 [ "rc_timestamp >= '20151212010101'" ],
590 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
595 'dir' => WatchedItemQueryService
::DIR_OLDER
,
596 'start' => '20151212020101',
597 'end' => '20151212010101'
602 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
603 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
607 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
611 [ "rc_timestamp >= '20151212010101'" ],
612 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
616 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
620 [ "rc_timestamp <= '20151212010101'" ],
621 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
626 'dir' => WatchedItemQueryService
::DIR_NEWER
,
627 'start' => '20151212010101',
628 'end' => '20151212020101'
633 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
634 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
647 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
656 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
665 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
674 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
683 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
692 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
701 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
710 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
714 [ 'rc_patrolled != 0' ],
719 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
723 [ 'rc_patrolled = 0' ],
728 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
732 [ 'rc_timestamp >= wl_notificationtimestamp' ],
737 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
741 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
746 [ 'onlyByUser' => 'SomeOtherUser' ],
750 [ 'rc_user_text' => 'SomeOtherUser' ],
755 [ 'notByUser' => 'SomeOtherUser' ],
759 [ "rc_user_text != 'SomeOtherUser'" ],
764 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
765 [ '20151212010101', 123 ],
769 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
771 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
775 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
776 [ '20151212010101', 123 ],
780 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
782 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
786 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
787 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
791 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
793 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
800 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
802 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
805 array $expectedExtraTables,
806 array $expectedExtraFields,
807 array $expectedExtraConds,
808 array $expectedDbOptions,
809 array $expectedExtraJoinConds
811 $this->overrideCommentStore();
813 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
814 $expectedFields = array_merge(
822 'wl_notificationtimestamp',
830 $expectedConds = array_merge(
831 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
834 $expectedJoinConds = array_merge(
839 'wl_namespace=rc_namespace',
848 $expectedExtraJoinConds
851 $mockDb = $this->getMockDb();
852 $mockDb->expects( $this->once() )
858 $this->isType( 'string' ),
862 ->will( $this->returnValue( [] ) );
864 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
865 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
867 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
869 $this->assertEmpty( $items );
870 $this->assertNull( $startFrom );
873 public function filterPatrolledOptionProvider() {
875 [ WatchedItemQueryService
::FILTER_PATROLLED
],
876 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
881 * @dataProvider filterPatrolledOptionProvider
883 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
886 $mockDb = $this->getMockDb();
887 $mockDb->expects( $this->once() )
890 [ 'recentchanges', 'watchlist', 'page' ],
891 $this->isType( 'array' ),
892 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
893 $this->isType( 'string' ),
894 $this->isType( 'array' ),
895 $this->isType( 'array' )
897 ->will( $this->returnValue( [] ) );
899 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
901 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
902 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
904 [ 'filters' => [ $filtersOption ] ]
907 $this->assertEmpty( $items );
910 public function mysqlIndexOptimizationProvider() {
915 [ "rc_timestamp > ''" ],
919 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
920 [ "rc_timestamp <= '20151212010101'" ],
924 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
925 [ "rc_timestamp >= '20151212010101'" ],
936 * @dataProvider mysqlIndexOptimizationProvider
938 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
941 array $expectedExtraConds
943 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
944 $conds = array_merge( $commonConds, $expectedExtraConds );
946 $mockDb = $this->getMockDb();
947 $mockDb->expects( $this->once() )
950 [ 'recentchanges', 'watchlist', 'page' ],
951 $this->isType( 'array' ),
953 $this->isType( 'string' ),
954 $this->isType( 'array' ),
955 $this->isType( 'array' )
957 ->will( $this->returnValue( [] ) );
958 $mockDb->expects( $this->any() )
959 ->method( 'getType' )
960 ->will( $this->returnValue( $dbType ) );
962 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
963 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
965 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
967 $this->assertEmpty( $items );
970 public function userPermissionRelatedExtraChecksProvider() {
976 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
977 LogPage
::DELETED_ACTION
. ')'
984 '(rc_type != ' . RC_LOG
. ') OR (' .
985 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
986 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
993 '(rc_type != ' . RC_LOG
. ') OR (' .
994 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
995 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
999 [ 'onlyByUser' => 'SomeOtherUser' ],
1002 'rc_user_text' => 'SomeOtherUser',
1003 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1004 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1005 LogPage
::DELETED_ACTION
. ')'
1009 [ 'onlyByUser' => 'SomeOtherUser' ],
1012 'rc_user_text' => 'SomeOtherUser',
1013 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1014 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1015 '(rc_type != ' . RC_LOG
. ') OR (' .
1016 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1017 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
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
) . ')'
1036 * @dataProvider userPermissionRelatedExtraChecksProvider
1038 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1041 array $expectedExtraConds
1043 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1044 $conds = array_merge( $commonConds, $expectedExtraConds );
1046 $mockDb = $this->getMockDb();
1047 $mockDb->expects( $this->once() )
1048 ->method( 'select' )
1050 [ 'recentchanges', 'watchlist', 'page' ],
1051 $this->isType( 'array' ),
1053 $this->isType( 'string' ),
1054 $this->isType( 'array' ),
1055 $this->isType( 'array' )
1057 ->will( $this->returnValue( [] ) );
1059 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1061 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1062 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1064 $this->assertEmpty( $items );
1067 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1068 $mockDb = $this->getMockDb();
1069 $mockDb->expects( $this->once() )
1070 ->method( 'select' )
1072 [ 'recentchanges', 'watchlist' ],
1080 'wl_notificationtimestamp',
1086 [ 'wl_user' => 1, ],
1087 $this->isType( 'string' ),
1093 'wl_namespace=rc_namespace',
1099 ->will( $this->returnValue( [] ) );
1101 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1102 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1104 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1106 $this->assertEmpty( $items );
1109 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1112 [ 'rcTypes' => [ 1337 ] ],
1114 'Bad value for parameter $options[\'rcTypes\']',
1117 [ 'rcTypes' => [ 'edit' ] ],
1119 'Bad value for parameter $options[\'rcTypes\']',
1122 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1124 'Bad value for parameter $options[\'rcTypes\']',
1129 'Bad value for parameter $options[\'dir\']',
1132 [ 'start' => '20151212010101' ],
1134 'Bad value for parameter $options[\'dir\']: must be provided',
1137 [ 'end' => '20151212010101' ],
1139 'Bad value for parameter $options[\'dir\']: must be provided',
1143 [ '20151212010101', 123 ],
1144 'Bad value for parameter $options[\'dir\']: must be provided',
1147 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1149 'Bad value for parameter $startFrom: must be a two-element array',
1152 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1153 [ '20151212010101' ],
1154 'Bad value for parameter $startFrom: must be a two-element array',
1157 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1158 [ '20151212010101', 123, 'foo' ],
1159 'Bad value for parameter $startFrom: must be a two-element array',
1162 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1164 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1167 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1169 'Bad value for parameter $options[\'watchlistOwner\']',
1175 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1177 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1180 $expectedInExceptionMessage
1182 $mockDb = $this->getMockDb();
1183 $mockDb->expects( $this->never() )
1184 ->method( $this->anything() );
1186 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1187 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1189 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1190 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1193 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1194 $mockDb = $this->getMockDb();
1195 $mockDb->expects( $this->once() )
1196 ->method( 'select' )
1198 [ 'recentchanges', 'watchlist', 'page' ],
1206 'wl_notificationtimestamp',
1209 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1210 $this->isType( 'string' ),
1216 'wl_namespace=rc_namespace',
1222 'rc_cur_id=page_id',
1226 ->will( $this->returnValue( [] ) );
1228 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1229 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1231 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1233 [ 'usedInGenerator' => true ]
1236 $this->assertEmpty( $items );
1239 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1240 $mockDb = $this->getMockDb();
1241 $mockDb->expects( $this->once() )
1242 ->method( 'select' )
1244 [ 'recentchanges', 'watchlist' ],
1252 'wl_notificationtimestamp',
1256 $this->isType( 'string' ),
1262 'wl_namespace=rc_namespace',
1268 ->will( $this->returnValue( [] ) );
1270 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1271 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1273 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1275 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1278 $this->assertEmpty( $items );
1281 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1282 $mockDb = $this->getMockDb();
1283 $mockDb->expects( $this->once() )
1284 ->method( 'select' )
1286 $this->isType( 'array' ),
1287 $this->isType( 'array' ),
1290 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1292 $this->isType( 'string' ),
1293 $this->isType( 'array' ),
1294 $this->isType( 'array' )
1296 ->will( $this->returnValue( [] ) );
1298 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1299 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1300 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1301 $otherUser->expects( $this->once() )
1302 ->method( 'getOption' )
1303 ->with( 'watchlisttoken' )
1304 ->willReturn( '0123456789abcdef' );
1306 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1308 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1311 $this->assertEmpty( $items );
1314 public function invalidWatchlistTokenProvider() {
1322 * @dataProvider invalidWatchlistTokenProvider
1324 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1325 $mockDb = $this->getMockDb();
1326 $mockDb->expects( $this->never() )
1327 ->method( $this->anything() );
1329 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1330 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1331 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1332 $otherUser->expects( $this->once() )
1333 ->method( 'getOption' )
1334 ->with( 'watchlisttoken' )
1335 ->willReturn( '0123456789abcdef' );
1337 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1338 $queryService->getWatchedItemsWithRecentChangeInfo(
1340 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1344 public function testGetWatchedItemsForUser() {
1345 $mockDb = $this->getMockDb();
1346 $mockDb->expects( $this->once() )
1347 ->method( 'select' )
1350 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1353 ->will( $this->returnValue( [
1354 $this->getFakeRow( [
1355 'wl_namespace' => 0,
1356 'wl_title' => 'Foo1',
1357 'wl_notificationtimestamp' => '20151212010101',
1359 $this->getFakeRow( [
1360 'wl_namespace' => 1,
1361 'wl_title' => 'Foo2',
1362 'wl_notificationtimestamp' => null,
1366 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1367 $user = $this->getMockNonAnonUserWithId( 1 );
1369 $items = $queryService->getWatchedItemsForUser( $user );
1371 $this->assertInternalType( 'array', $items );
1372 $this->assertCount( 2, $items );
1373 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1374 $this->assertEquals(
1375 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1378 $this->assertEquals(
1379 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1384 public function provideGetWatchedItemsForUserOptions() {
1387 [ 'namespaceIds' => [ 0, 1 ], ],
1388 [ 'wl_namespace' => [ 0, 1 ], ],
1392 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1394 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1398 'namespaceIds' => [ 0 ],
1399 'sort' => WatchedItemQueryService
::SORT_ASC
,
1401 [ 'wl_namespace' => [ 0 ], ],
1402 [ 'ORDER BY' => 'wl_title ASC' ]
1411 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1412 'limit' => "10; DROP TABLE watchlist;\n--",
1414 [ 'wl_namespace' => [ 0, 1 ], ],
1418 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1419 [ 'wl_notificationtimestamp IS NOT NULL' ],
1423 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1424 [ 'wl_notificationtimestamp IS NULL' ],
1428 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1430 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1434 'namespaceIds' => [ 0 ],
1435 'sort' => WatchedItemQueryService
::SORT_DESC
,
1437 [ 'wl_namespace' => [ 0 ], ],
1438 [ 'ORDER BY' => 'wl_title DESC' ]
1444 * @dataProvider provideGetWatchedItemsForUserOptions
1446 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1448 array $expectedConds,
1449 array $expectedDbOptions
1451 $mockDb = $this->getMockDb();
1452 $user = $this->getMockNonAnonUserWithId( 1 );
1454 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1455 $mockDb->expects( $this->once() )
1456 ->method( 'select' )
1459 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1461 $this->isType( 'string' ),
1464 ->will( $this->returnValue( [] ) );
1466 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1468 $items = $queryService->getWatchedItemsForUser( $user, $options );
1469 $this->assertEmpty( $items );
1472 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1476 'from' => new TitleValue( 0, 'SomeDbKey' ),
1477 'sort' => WatchedItemQueryService
::SORT_ASC
1479 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1480 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1484 'from' => new TitleValue( 0, 'SomeDbKey' ),
1485 'sort' => WatchedItemQueryService
::SORT_DESC
,
1487 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1488 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1492 'until' => new TitleValue( 0, 'SomeDbKey' ),
1493 'sort' => WatchedItemQueryService
::SORT_ASC
1495 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1496 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1500 'until' => new TitleValue( 0, 'SomeDbKey' ),
1501 'sort' => WatchedItemQueryService
::SORT_DESC
1503 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1504 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1508 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1509 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1510 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1511 'sort' => WatchedItemQueryService
::SORT_ASC
1514 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1515 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1516 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1518 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1522 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1523 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1524 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1525 'sort' => WatchedItemQueryService
::SORT_DESC
1528 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1529 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1530 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1532 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1538 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1540 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1542 array $expectedConds,
1543 array $expectedDbOptions
1545 $user = $this->getMockNonAnonUserWithId( 1 );
1547 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1549 $mockDb = $this->getMockDb();
1550 $mockDb->expects( $this->any() )
1551 ->method( 'addQuotes' )
1552 ->will( $this->returnCallback( function ( $value ) {
1555 $mockDb->expects( $this->any() )
1556 ->method( 'makeList' )
1558 $this->isType( 'array' ),
1559 $this->isType( 'int' )
1561 ->will( $this->returnCallback( function ( $a, $conj ) {
1562 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1563 return join( $sqlConj, array_map( function ( $s ) {
1564 return '(' . $s . ')';
1568 $mockDb->expects( $this->once() )
1569 ->method( 'select' )
1572 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1574 $this->isType( 'string' ),
1577 ->will( $this->returnValue( [] ) );
1579 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1581 $items = $queryService->getWatchedItemsForUser( $user, $options );
1582 $this->assertEmpty( $items );
1585 public function getWatchedItemsForUserInvalidOptionsProvider() {
1588 [ 'sort' => 'foo' ],
1589 'Bad value for parameter $options[\'sort\']'
1592 [ 'filter' => 'foo' ],
1593 'Bad value for parameter $options[\'filter\']'
1596 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1597 'Bad value for parameter $options[\'sort\']: must be provided'
1600 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1601 'Bad value for parameter $options[\'sort\']: must be provided'
1604 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1605 'Bad value for parameter $options[\'sort\']: must be provided'
1611 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1613 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1615 $expectedInExceptionMessage
1617 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
1619 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1620 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1623 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1624 $mockDb = $this->getMockDb();
1626 $mockDb->expects( $this->never() )
1627 ->method( $this->anything() );
1629 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1631 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1632 $this->assertEmpty( $items );