3 use Wikimedia\TestingAccessWrapper
;
6 * @covers WatchedItemQueryService
8 class WatchedItemQueryServiceUnitTest
extends PHPUnit_Framework_TestCase
{
11 * @return PHPUnit_Framework_MockObject_MockObject|Database
13 private function getMockDb() {
14 $mock = $this->getMockBuilder( Database
::class )
15 ->disableOriginalConstructor()
18 $mock->expects( $this->any() )
19 ->method( 'makeList' )
21 $this->isType( 'array' ),
22 $this->isType( 'int' )
24 ->will( $this->returnCallback( function ( $a, $conj ) {
25 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
26 return join( $sqlConj, array_map( function ( $s ) {
27 return '(' . $s . ')';
32 $mock->expects( $this->any() )
33 ->method( 'addQuotes' )
34 ->will( $this->returnCallback( function ( $value ) {
38 $mock->expects( $this->any() )
39 ->method( 'timestamp' )
40 ->will( $this->returnArgument( 0 ) );
42 $mock->expects( $this->any() )
44 ->willReturnCallback( function ( $a, $b ) {
52 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
53 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
55 private function getMockLoadBalancer( $mockDb ) {
56 $mock = $this->getMockBuilder( LoadBalancer
::class )
57 ->disableOriginalConstructor()
59 $mock->expects( $this->any() )
60 ->method( 'getConnectionRef' )
62 ->will( $this->returnValue( $mockDb ) );
68 * @return PHPUnit_Framework_MockObject_MockObject|User
70 private function getMockNonAnonUserWithId( $id ) {
71 $mock = $this->getMockBuilder( User
::class )->getMock();
72 $mock->expects( $this->any() )
74 ->will( $this->returnValue( false ) );
75 $mock->expects( $this->any() )
77 ->will( $this->returnValue( $id ) );
83 * @return PHPUnit_Framework_MockObject_MockObject|User
85 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
86 $mock = $this->getMockNonAnonUserWithId( $id );
87 $mock->expects( $this->any() )
88 ->method( 'isAllowed' )
89 ->will( $this->returnValue( true ) );
90 $mock->expects( $this->any() )
91 ->method( 'isAllowedAny' )
92 ->will( $this->returnValue( true ) );
93 $mock->expects( $this->any() )
94 ->method( 'useRCPatrol' )
95 ->will( $this->returnValue( true ) );
101 * @param string $notAllowedAction
102 * @return PHPUnit_Framework_MockObject_MockObject|User
104 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
105 $mock = $this->getMockNonAnonUserWithId( $id );
107 $mock->expects( $this->any() )
108 ->method( 'isAllowed' )
109 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
110 return $action !== $notAllowedAction;
112 $mock->expects( $this->any() )
113 ->method( 'isAllowedAny' )
114 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
115 $actions = func_get_args();
116 return !in_array( $notAllowedAction, $actions );
124 * @return PHPUnit_Framework_MockObject_MockObject|User
126 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
127 $mock = $this->getMockNonAnonUserWithId( $id );
129 $mock->expects( $this->any() )
130 ->method( 'isAllowed' )
131 ->will( $this->returnValue( true ) );
132 $mock->expects( $this->any() )
133 ->method( 'isAllowedAny' )
134 ->will( $this->returnValue( true ) );
136 $mock->expects( $this->any() )
137 ->method( 'useRCPatrol' )
138 ->will( $this->returnValue( false ) );
139 $mock->expects( $this->any() )
140 ->method( 'useNPPatrol' )
141 ->will( $this->returnValue( false ) );
146 private function getMockAnonUser() {
147 $mock = $this->getMockBuilder( User
::class )->getMock();
148 $mock->expects( $this->any() )
150 ->will( $this->returnValue( true ) );
154 private function getFakeRow( array $rowValues ) {
155 $fakeRow = new stdClass();
156 foreach ( $rowValues as $valueName => $value ) {
157 $fakeRow->$valueName = $value;
162 public function testGetWatchedItemsWithRecentChangeInfo() {
163 $mockDb = $this->getMockDb();
164 $mockDb->expects( $this->once() )
167 [ 'recentchanges', 'watchlist', 'page' ],
175 'wl_notificationtimestamp',
182 '(rc_this_oldid=page_latest) OR (rc_type=3)',
184 $this->isType( 'string' ),
192 'wl_namespace=rc_namespace',
202 ->will( $this->returnValue( [
206 'rc_title' => 'Foo1',
207 'rc_timestamp' => '20151212010101',
210 'wl_notificationtimestamp' => '20151212010101',
215 'rc_title' => 'Foo2',
216 'rc_timestamp' => '20151212010102',
219 'wl_notificationtimestamp' => null,
224 'rc_title' => 'Foo3',
225 'rc_timestamp' => '20151212010103',
228 'wl_notificationtimestamp' => null,
232 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
233 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
236 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
237 $user, [ 'limit' => 2 ], $startFrom
240 $this->assertInternalType( 'array', $items );
241 $this->assertCount( 2, $items );
243 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
244 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
245 $this->assertInternalType( 'array', $recentChangeInfo );
249 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
256 'rc_title' => 'Foo1',
257 'rc_timestamp' => '20151212010101',
265 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
272 'rc_title' => 'Foo2',
273 'rc_timestamp' => '20151212010102',
280 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
283 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
284 $mockDb = $this->getMockDb();
285 $mockDb->expects( $this->once() )
288 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
296 'wl_notificationtimestamp',
300 'extension_dummy_field',
304 '(rc_this_oldid=page_latest) OR (rc_type=3)',
305 'extension_dummy_cond',
307 $this->isType( 'string' ),
309 'extension_dummy_option',
315 'wl_namespace=rc_namespace',
323 'extension_dummy_join_cond' => [],
326 ->will( $this->returnValue( [
330 'rc_title' => 'Foo1',
331 'rc_timestamp' => '20151212010101',
334 'wl_notificationtimestamp' => '20151212010101',
339 'rc_title' => 'Foo2',
340 'rc_timestamp' => '20151212010102',
343 'wl_notificationtimestamp' => null,
347 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
349 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
351 $mockExtension->expects( $this->once() )
352 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
354 $this->identicalTo( $user ),
355 $this->isType( 'array' ),
356 $this->isInstanceOf( IDatabase
::class ),
357 $this->isType( 'array' ),
358 $this->isType( 'array' ),
359 $this->isType( 'array' ),
360 $this->isType( 'array' ),
361 $this->isType( 'array' )
363 ->will( $this->returnCallback( function (
364 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
366 $tables[] = 'extension_dummy_table';
367 $fields[] = 'extension_dummy_field';
368 $conds[] = 'extension_dummy_cond';
369 $dbOptions[] = 'extension_dummy_option';
370 $joinConds['extension_dummy_join_cond'] = [];
372 $mockExtension->expects( $this->once() )
373 ->method( 'modifyWatchedItemsWithRCInfo' )
375 $this->identicalTo( $user ),
376 $this->isType( 'array' ),
377 $this->isInstanceOf( IDatabase
::class ),
378 $this->isType( 'array' ),
380 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
382 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
383 foreach ( $items as $i => &$item ) {
384 $item[1]['extension_dummy_field'] = $i;
388 $this->assertNull( $startFrom );
389 $startFrom = [ '20160203123456', 42 ];
392 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
393 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
396 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
397 $user, [], $startFrom
400 $this->assertInternalType( 'array', $items );
401 $this->assertCount( 2, $items );
403 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
404 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
405 $this->assertInternalType( 'array', $recentChangeInfo );
409 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
416 'rc_title' => 'Foo1',
417 'rc_timestamp' => '20151212010101',
420 'extension_dummy_field' => 0,
426 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
433 'rc_title' => 'Foo2',
434 'rc_timestamp' => '20151212010102',
437 'extension_dummy_field' => 1,
442 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
445 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
448 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
450 [ 'rc_type', 'rc_minor', 'rc_bot' ],
455 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
462 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
469 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
476 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
478 [ 'rc_patrolled', 'rc_log_type' ],
483 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
485 [ 'rc_old_len', 'rc_new_len' ],
490 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
492 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
497 [ 'namespaceIds' => [ 0, 1 ] ],
500 [ 'wl_namespace' => [ 0, 1 ] ],
504 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
507 [ 'wl_namespace' => [ 0, 1 ] ],
511 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
514 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
518 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
522 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
525 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
529 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
532 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
535 [ "rc_timestamp <= '20151212010101'" ],
536 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
539 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
542 [ "rc_timestamp >= '20151212010101'" ],
543 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
547 'dir' => WatchedItemQueryService
::DIR_OLDER
,
548 'start' => '20151212020101',
549 'end' => '20151212010101'
553 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
554 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
557 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
560 [ "rc_timestamp >= '20151212010101'" ],
561 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
564 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
567 [ "rc_timestamp <= '20151212010101'" ],
568 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
572 'dir' => WatchedItemQueryService
::DIR_NEWER
,
573 'start' => '20151212010101',
574 'end' => '20151212020101'
578 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
579 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
589 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
596 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
603 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
610 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
617 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
624 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
631 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
638 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
641 [ 'rc_patrolled != 0' ],
645 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
648 [ 'rc_patrolled = 0' ],
652 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
655 [ 'rc_timestamp >= wl_notificationtimestamp' ],
659 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
662 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
666 [ 'onlyByUser' => 'SomeOtherUser' ],
669 [ 'rc_user_text' => 'SomeOtherUser' ],
673 [ 'notByUser' => 'SomeOtherUser' ],
676 [ "rc_user_text != 'SomeOtherUser'" ],
680 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
681 [ '20151212010101', 123 ],
684 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
686 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
689 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
690 [ '20151212010101', 123 ],
693 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
695 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
698 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
699 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
702 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
704 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
710 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
712 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
715 array $expectedExtraFields,
716 array $expectedExtraConds,
717 array $expectedDbOptions
719 $expectedFields = array_merge(
727 'wl_notificationtimestamp',
735 $expectedConds = array_merge(
736 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
740 $mockDb = $this->getMockDb();
741 $mockDb->expects( $this->once() )
744 [ 'recentchanges', 'watchlist', 'page' ],
747 $this->isType( 'string' ),
753 'wl_namespace=rc_namespace',
763 ->will( $this->returnValue( [] ) );
765 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
766 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
768 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
770 $this->assertEmpty( $items );
771 $this->assertNull( $startFrom );
774 public function filterPatrolledOptionProvider() {
776 [ WatchedItemQueryService
::FILTER_PATROLLED
],
777 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
782 * @dataProvider filterPatrolledOptionProvider
784 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
787 $mockDb = $this->getMockDb();
788 $mockDb->expects( $this->once() )
791 [ 'recentchanges', 'watchlist', 'page' ],
792 $this->isType( 'array' ),
793 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
794 $this->isType( 'string' ),
795 $this->isType( 'array' ),
796 $this->isType( 'array' )
798 ->will( $this->returnValue( [] ) );
800 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
802 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
803 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
805 [ 'filters' => [ $filtersOption ] ]
808 $this->assertEmpty( $items );
811 public function mysqlIndexOptimizationProvider() {
816 [ "rc_timestamp > ''" ],
820 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
821 [ "rc_timestamp <= '20151212010101'" ],
825 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
826 [ "rc_timestamp >= '20151212010101'" ],
837 * @dataProvider mysqlIndexOptimizationProvider
839 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
842 array $expectedExtraConds
844 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
845 $conds = array_merge( $commonConds, $expectedExtraConds );
847 $mockDb = $this->getMockDb();
848 $mockDb->expects( $this->once() )
851 [ 'recentchanges', 'watchlist', 'page' ],
852 $this->isType( 'array' ),
854 $this->isType( 'string' ),
855 $this->isType( 'array' ),
856 $this->isType( 'array' )
858 ->will( $this->returnValue( [] ) );
859 $mockDb->expects( $this->any() )
860 ->method( 'getType' )
861 ->will( $this->returnValue( $dbType ) );
863 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
864 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
866 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
868 $this->assertEmpty( $items );
871 public function userPermissionRelatedExtraChecksProvider() {
877 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
878 LogPage
::DELETED_ACTION
. ')'
885 '(rc_type != ' . RC_LOG
. ') OR (' .
886 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
887 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
894 '(rc_type != ' . RC_LOG
. ') OR (' .
895 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
896 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
900 [ 'onlyByUser' => 'SomeOtherUser' ],
903 'rc_user_text' => 'SomeOtherUser',
904 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
905 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
906 LogPage
::DELETED_ACTION
. ')'
910 [ 'onlyByUser' => 'SomeOtherUser' ],
913 'rc_user_text' => 'SomeOtherUser',
914 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
915 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
916 '(rc_type != ' . RC_LOG
. ') OR (' .
917 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
918 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
922 [ 'onlyByUser' => 'SomeOtherUser' ],
925 'rc_user_text' => 'SomeOtherUser',
926 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
927 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
928 '(rc_type != ' . RC_LOG
. ') OR (' .
929 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
930 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
937 * @dataProvider userPermissionRelatedExtraChecksProvider
939 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
942 array $expectedExtraConds
944 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
945 $conds = array_merge( $commonConds, $expectedExtraConds );
947 $mockDb = $this->getMockDb();
948 $mockDb->expects( $this->once() )
951 [ 'recentchanges', 'watchlist', 'page' ],
952 $this->isType( 'array' ),
954 $this->isType( 'string' ),
955 $this->isType( 'array' ),
956 $this->isType( 'array' )
958 ->will( $this->returnValue( [] ) );
960 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
962 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
963 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
965 $this->assertEmpty( $items );
968 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
969 $mockDb = $this->getMockDb();
970 $mockDb->expects( $this->once() )
973 [ 'recentchanges', 'watchlist' ],
981 'wl_notificationtimestamp',
988 $this->isType( 'string' ),
994 'wl_namespace=rc_namespace',
1000 ->will( $this->returnValue( [] ) );
1002 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1003 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1005 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1007 $this->assertEmpty( $items );
1010 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1013 [ 'rcTypes' => [ 1337 ] ],
1015 'Bad value for parameter $options[\'rcTypes\']',
1018 [ 'rcTypes' => [ 'edit' ] ],
1020 'Bad value for parameter $options[\'rcTypes\']',
1023 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1025 'Bad value for parameter $options[\'rcTypes\']',
1030 'Bad value for parameter $options[\'dir\']',
1033 [ 'start' => '20151212010101' ],
1035 'Bad value for parameter $options[\'dir\']: must be provided',
1038 [ 'end' => '20151212010101' ],
1040 'Bad value for parameter $options[\'dir\']: must be provided',
1044 [ '20151212010101', 123 ],
1045 'Bad value for parameter $options[\'dir\']: must be provided',
1048 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1050 'Bad value for parameter $startFrom: must be a two-element array',
1053 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1054 [ '20151212010101' ],
1055 'Bad value for parameter $startFrom: must be a two-element array',
1058 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1059 [ '20151212010101', 123, 'foo' ],
1060 'Bad value for parameter $startFrom: must be a two-element array',
1063 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1065 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1068 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1070 'Bad value for parameter $options[\'watchlistOwner\']',
1076 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1078 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1081 $expectedInExceptionMessage
1083 $mockDb = $this->getMockDb();
1084 $mockDb->expects( $this->never() )
1085 ->method( $this->anything() );
1087 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1088 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1090 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1091 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1094 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1095 $mockDb = $this->getMockDb();
1096 $mockDb->expects( $this->once() )
1097 ->method( 'select' )
1099 [ 'recentchanges', 'watchlist', 'page' ],
1107 'wl_notificationtimestamp',
1110 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1111 $this->isType( 'string' ),
1117 'wl_namespace=rc_namespace',
1123 'rc_cur_id=page_id',
1127 ->will( $this->returnValue( [] ) );
1129 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1130 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1132 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1134 [ 'usedInGenerator' => true ]
1137 $this->assertEmpty( $items );
1140 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1141 $mockDb = $this->getMockDb();
1142 $mockDb->expects( $this->once() )
1143 ->method( 'select' )
1145 [ 'recentchanges', 'watchlist' ],
1153 'wl_notificationtimestamp',
1157 $this->isType( 'string' ),
1163 'wl_namespace=rc_namespace',
1169 ->will( $this->returnValue( [] ) );
1171 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1172 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1174 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1176 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1179 $this->assertEmpty( $items );
1182 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1183 $mockDb = $this->getMockDb();
1184 $mockDb->expects( $this->once() )
1185 ->method( 'select' )
1187 $this->isType( 'array' ),
1188 $this->isType( 'array' ),
1191 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1193 $this->isType( 'string' ),
1194 $this->isType( 'array' ),
1195 $this->isType( 'array' )
1197 ->will( $this->returnValue( [] ) );
1199 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1200 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1201 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1202 $otherUser->expects( $this->once() )
1203 ->method( 'getOption' )
1204 ->with( 'watchlisttoken' )
1205 ->willReturn( '0123456789abcdef' );
1207 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1209 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1212 $this->assertEmpty( $items );
1215 public function invalidWatchlistTokenProvider() {
1223 * @dataProvider invalidWatchlistTokenProvider
1225 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1226 $mockDb = $this->getMockDb();
1227 $mockDb->expects( $this->never() )
1228 ->method( $this->anything() );
1230 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1231 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1232 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1233 $otherUser->expects( $this->once() )
1234 ->method( 'getOption' )
1235 ->with( 'watchlisttoken' )
1236 ->willReturn( '0123456789abcdef' );
1238 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1239 $queryService->getWatchedItemsWithRecentChangeInfo(
1241 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1245 public function testGetWatchedItemsForUser() {
1246 $mockDb = $this->getMockDb();
1247 $mockDb->expects( $this->once() )
1248 ->method( 'select' )
1251 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1254 ->will( $this->returnValue( [
1255 $this->getFakeRow( [
1256 'wl_namespace' => 0,
1257 'wl_title' => 'Foo1',
1258 'wl_notificationtimestamp' => '20151212010101',
1260 $this->getFakeRow( [
1261 'wl_namespace' => 1,
1262 'wl_title' => 'Foo2',
1263 'wl_notificationtimestamp' => null,
1267 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1268 $user = $this->getMockNonAnonUserWithId( 1 );
1270 $items = $queryService->getWatchedItemsForUser( $user );
1272 $this->assertInternalType( 'array', $items );
1273 $this->assertCount( 2, $items );
1274 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1275 $this->assertEquals(
1276 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1279 $this->assertEquals(
1280 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1285 public function provideGetWatchedItemsForUserOptions() {
1288 [ 'namespaceIds' => [ 0, 1 ], ],
1289 [ 'wl_namespace' => [ 0, 1 ], ],
1293 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1295 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1299 'namespaceIds' => [ 0 ],
1300 'sort' => WatchedItemQueryService
::SORT_ASC
,
1302 [ 'wl_namespace' => [ 0 ], ],
1303 [ 'ORDER BY' => 'wl_title ASC' ]
1312 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1313 'limit' => "10; DROP TABLE watchlist;\n--",
1315 [ 'wl_namespace' => [ 0, 1 ], ],
1319 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1320 [ 'wl_notificationtimestamp IS NOT NULL' ],
1324 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1325 [ 'wl_notificationtimestamp IS NULL' ],
1329 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1331 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1335 'namespaceIds' => [ 0 ],
1336 'sort' => WatchedItemQueryService
::SORT_DESC
,
1338 [ 'wl_namespace' => [ 0 ], ],
1339 [ 'ORDER BY' => 'wl_title DESC' ]
1345 * @dataProvider provideGetWatchedItemsForUserOptions
1347 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1349 array $expectedConds,
1350 array $expectedDbOptions
1352 $mockDb = $this->getMockDb();
1353 $user = $this->getMockNonAnonUserWithId( 1 );
1355 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1356 $mockDb->expects( $this->once() )
1357 ->method( 'select' )
1360 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1362 $this->isType( 'string' ),
1365 ->will( $this->returnValue( [] ) );
1367 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1369 $items = $queryService->getWatchedItemsForUser( $user, $options );
1370 $this->assertEmpty( $items );
1373 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1377 'from' => new TitleValue( 0, 'SomeDbKey' ),
1378 'sort' => WatchedItemQueryService
::SORT_ASC
1380 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1381 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1385 'from' => new TitleValue( 0, 'SomeDbKey' ),
1386 'sort' => WatchedItemQueryService
::SORT_DESC
,
1388 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1389 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1393 'until' => new TitleValue( 0, 'SomeDbKey' ),
1394 'sort' => WatchedItemQueryService
::SORT_ASC
1396 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1397 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1401 'until' => new TitleValue( 0, 'SomeDbKey' ),
1402 'sort' => WatchedItemQueryService
::SORT_DESC
1404 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1405 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1409 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1410 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1411 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1412 'sort' => WatchedItemQueryService
::SORT_ASC
1415 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1416 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1417 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1419 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1423 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1424 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1425 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1426 'sort' => WatchedItemQueryService
::SORT_DESC
1429 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1430 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1431 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1433 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1439 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1441 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1443 array $expectedConds,
1444 array $expectedDbOptions
1446 $user = $this->getMockNonAnonUserWithId( 1 );
1448 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1450 $mockDb = $this->getMockDb();
1451 $mockDb->expects( $this->any() )
1452 ->method( 'addQuotes' )
1453 ->will( $this->returnCallback( function ( $value ) {
1456 $mockDb->expects( $this->any() )
1457 ->method( 'makeList' )
1459 $this->isType( 'array' ),
1460 $this->isType( 'int' )
1462 ->will( $this->returnCallback( function ( $a, $conj ) {
1463 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1464 return join( $sqlConj, array_map( function ( $s ) {
1465 return '(' . $s . ')';
1469 $mockDb->expects( $this->once() )
1470 ->method( 'select' )
1473 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1475 $this->isType( 'string' ),
1478 ->will( $this->returnValue( [] ) );
1480 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1482 $items = $queryService->getWatchedItemsForUser( $user, $options );
1483 $this->assertEmpty( $items );
1486 public function getWatchedItemsForUserInvalidOptionsProvider() {
1489 [ 'sort' => 'foo' ],
1490 'Bad value for parameter $options[\'sort\']'
1493 [ 'filter' => 'foo' ],
1494 'Bad value for parameter $options[\'filter\']'
1497 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1498 'Bad value for parameter $options[\'sort\']: must be provided'
1501 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1502 'Bad value for parameter $options[\'sort\']: must be provided'
1505 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1506 'Bad value for parameter $options[\'sort\']: must be provided'
1512 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1514 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1516 $expectedInExceptionMessage
1518 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
1520 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1521 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1524 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1525 $mockDb = $this->getMockDb();
1527 $mockDb->expects( $this->never() )
1528 ->method( $this->anything() );
1530 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1532 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1533 $this->assertEmpty( $items );