3 use MediaWiki\User\UserIdentityValue
;
4 use Wikimedia\Rdbms\IDatabase
;
5 use Wikimedia\Rdbms\LoadBalancer
;
6 use Wikimedia\TestingAccessWrapper
;
9 * @covers WatchedItemQueryService
11 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
13 use MediaWikiCoversValidator
;
16 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
18 private function getMockCommentStore() {
19 $mockStore = $this->getMockBuilder( CommentStore
::class )
20 ->disableOriginalConstructor()
22 $mockStore->expects( $this->any() )
23 ->method( 'getFields' )
24 ->willReturn( [ 'commentstore' => 'fields' ] );
25 $mockStore->expects( $this->any() )
28 'tables' => [ 'commentstore' => 'table' ],
29 'fields' => [ 'commentstore' => 'field' ],
30 'joins' => [ 'commentstore' => 'join' ],
36 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
38 private function getMockActorMigration() {
39 $mockStore = $this->getMockBuilder( ActorMigration
::class )
40 ->disableOriginalConstructor()
42 $mockStore->expects( $this->any() )
45 'tables' => [ 'actormigration' => 'table' ],
47 'rc_user' => 'actormigration_user',
48 'rc_user_text' => 'actormigration_user_text',
49 'rc_actor' => 'actormigration_actor',
51 'joins' => [ 'actormigration' => 'join' ],
53 $mockStore->expects( $this->any() )
54 ->method( 'getWhere' )
56 'tables' => [ 'actormigration' => 'table' ],
57 'conds' => 'actormigration_conds',
58 'joins' => [ 'actormigration' => 'join' ],
60 $mockStore->expects( $this->any() )
62 ->willReturn( 'actormigration is anon' );
63 $mockStore->expects( $this->any() )
64 ->method( 'isNotAnon' )
65 ->willReturn( 'actormigration is not anon' );
70 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
71 * @return WatchedItemQueryService
73 private function newService( $mockDb ) {
74 return new WatchedItemQueryService(
75 $this->getMockLoadBalancer( $mockDb ),
76 $this->getMockCommentStore(),
77 $this->getMockActorMigration(),
78 $this->getMockWatchedItemStore()
83 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
85 private function getMockDb() {
86 $mock = $this->createMock( IDatabase
::class );
88 $mock->expects( $this->any() )
89 ->method( 'makeList' )
91 $this->isType( 'array' ),
92 $this->isType( 'int' )
94 ->will( $this->returnCallback( function ( $a, $conj ) {
95 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
97 foreach ( $a as $k => $v ) {
100 } elseif ( is_array( $v ) ) {
101 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
103 $conds[] = "($k = '$v')";
106 return implode( $sqlConj, $conds );
109 $mock->expects( $this->any() )
110 ->method( 'addQuotes' )
111 ->will( $this->returnCallback( function ( $value ) {
115 $mock->expects( $this->any() )
116 ->method( 'timestamp' )
117 ->will( $this->returnArgument( 0 ) );
119 $mock->expects( $this->any() )
121 ->willReturnCallback( function ( $a, $b ) {
129 * @param PHPUnit_Framework_MockObject_MockObject|IDatabase $mockDb
130 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
132 private function getMockLoadBalancer( $mockDb ) {
133 $mock = $this->getMockBuilder( LoadBalancer
::class )
134 ->disableOriginalConstructor()
136 $mock->expects( $this->any() )
137 ->method( 'getConnectionRef' )
139 ->will( $this->returnValue( $mockDb ) );
144 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
146 private function getMockWatchedItemStore() {
147 $mock = $this->getMockBuilder( WatchedItemStore
::class )
148 ->disableOriginalConstructor()
150 $mock->expects( $this->any() )
151 ->method( 'getLatestNotificationTimestamp' )
152 ->will( $this->returnCallback( function ( $timestamp ) {
160 * @param string[] $extraMethods Extra methods that are expected might be called
161 * @return PHPUnit_Framework_MockObject_MockObject|User
163 private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
164 $mock = $this->getMockBuilder( User
::class )->getMock();
165 $mock->method( 'isRegistered' )->willReturn( true );
166 $mock->method( 'getId' )->willReturn( $id );
167 $methods = array_merge( [
171 $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) );
177 * @param string[] $extraMethods Extra methods that are expected might be called
178 * @return PHPUnit_Framework_MockObject_MockObject|User
180 private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
181 $mock = $this->getMockNonAnonUserWithId( $id,
182 array_merge( [ 'isAllowed', 'isAllowedAny', 'useRCPatrol' ], $extraMethods ) );
183 $mock->method( 'isAllowed' )->willReturn( true );
184 $mock->method( 'isAllowedAny' )->willReturn( true );
185 $mock->method( 'useRCPatrol' )->willReturn( true );
191 * @param string $notAllowedAction
192 * @return PHPUnit_Framework_MockObject_MockObject|User
194 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
195 $mock = $this->getMockNonAnonUserWithId( $id,
196 [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
198 $mock->method( 'isAllowed' )
199 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
200 return $action !== $notAllowedAction;
202 $mock->method( 'isAllowedAny' )
203 ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
204 return !in_array( $notAllowedAction, $actions );
206 $mock->method( 'useRCPatrol' )->willReturn( false );
207 $mock->method( 'useNPPatrol' )->willReturn( false );
214 * @return PHPUnit_Framework_MockObject_MockObject|User
216 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
217 $mock = $this->getMockNonAnonUserWithId( $id,
218 [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
220 $mock->expects( $this->any() )
221 ->method( 'isAllowed' )
222 ->will( $this->returnValue( true ) );
223 $mock->expects( $this->any() )
224 ->method( 'isAllowedAny' )
225 ->will( $this->returnValue( true ) );
227 $mock->expects( $this->any() )
228 ->method( 'useRCPatrol' )
229 ->will( $this->returnValue( false ) );
230 $mock->expects( $this->any() )
231 ->method( 'useNPPatrol' )
232 ->will( $this->returnValue( false ) );
237 private function getFakeRow( array $rowValues ) {
238 $fakeRow = new stdClass();
239 foreach ( $rowValues as $valueName => $value ) {
240 $fakeRow->$valueName = $value;
245 public function testGetWatchedItemsWithRecentChangeInfo() {
246 $mockDb = $this->getMockDb();
247 $mockDb->expects( $this->once() )
250 [ 'recentchanges', 'watchlist', 'page' ],
258 'wl_notificationtimestamp',
265 '(rc_this_oldid=page_latest) OR (rc_type=3)',
267 $this->isType( 'string' ),
275 'wl_namespace=rc_namespace',
285 ->will( $this->returnValue( [
289 'rc_title' => 'Foo1',
290 'rc_timestamp' => '20151212010101',
293 'wl_notificationtimestamp' => '20151212010101',
298 'rc_title' => 'Foo2',
299 'rc_timestamp' => '20151212010102',
302 'wl_notificationtimestamp' => null,
307 'rc_title' => 'Foo3',
308 'rc_timestamp' => '20151212010103',
311 'wl_notificationtimestamp' => null,
315 $queryService = $this->newService( $mockDb );
316 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
319 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
320 $user, [ 'limit' => 2 ], $startFrom
323 $this->assertInternalType( 'array', $items );
324 $this->assertCount( 2, $items );
326 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
327 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
328 $this->assertInternalType( 'array', $recentChangeInfo );
332 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
339 'rc_title' => 'Foo1',
340 'rc_timestamp' => '20151212010101',
348 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
355 'rc_title' => 'Foo2',
356 'rc_timestamp' => '20151212010102',
363 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
366 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
367 $mockDb = $this->getMockDb();
368 $mockDb->expects( $this->once() )
371 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
379 'wl_notificationtimestamp',
383 'extension_dummy_field',
387 '(rc_this_oldid=page_latest) OR (rc_type=3)',
388 'extension_dummy_cond',
390 $this->isType( 'string' ),
392 'extension_dummy_option',
398 'wl_namespace=rc_namespace',
406 'extension_dummy_join_cond' => [],
409 ->will( $this->returnValue( [
413 'rc_title' => 'Foo1',
414 'rc_timestamp' => '20151212010101',
417 'wl_notificationtimestamp' => '20151212010101',
422 'rc_title' => 'Foo2',
423 'rc_timestamp' => '20151212010102',
426 'wl_notificationtimestamp' => null,
430 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
432 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
434 $mockExtension->expects( $this->once() )
435 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
437 $this->identicalTo( $user ),
438 $this->isType( 'array' ),
439 $this->isInstanceOf( IDatabase
::class ),
440 $this->isType( 'array' ),
441 $this->isType( 'array' ),
442 $this->isType( 'array' ),
443 $this->isType( 'array' ),
444 $this->isType( 'array' )
446 ->will( $this->returnCallback( function (
447 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
449 $tables[] = 'extension_dummy_table';
450 $fields[] = 'extension_dummy_field';
451 $conds[] = 'extension_dummy_cond';
452 $dbOptions[] = 'extension_dummy_option';
453 $joinConds['extension_dummy_join_cond'] = [];
455 $mockExtension->expects( $this->once() )
456 ->method( 'modifyWatchedItemsWithRCInfo' )
458 $this->identicalTo( $user ),
459 $this->isType( 'array' ),
460 $this->isInstanceOf( IDatabase
::class ),
461 $this->isType( 'array' ),
463 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
465 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
466 foreach ( $items as $i => &$item ) {
467 $item[1]['extension_dummy_field'] = $i;
471 $this->assertNull( $startFrom );
472 $startFrom = [ '20160203123456', 42 ];
475 $queryService = $this->newService( $mockDb );
476 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
479 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
480 $user, [], $startFrom
483 $this->assertInternalType( 'array', $items );
484 $this->assertCount( 2, $items );
486 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
487 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
488 $this->assertInternalType( 'array', $recentChangeInfo );
492 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
499 'rc_title' => 'Foo1',
500 'rc_timestamp' => '20151212010101',
503 'extension_dummy_field' => 0,
509 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
516 'rc_title' => 'Foo2',
517 'rc_timestamp' => '20151212010102',
520 'extension_dummy_field' => 1,
525 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
528 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
531 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
534 [ 'rc_type', 'rc_minor', 'rc_bot' ],
540 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
542 [ 'actormigration' => 'table' ],
543 [ 'rc_user_text' => 'actormigration_user_text' ],
546 [ 'actormigration' => 'join' ],
549 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
551 [ 'actormigration' => 'table' ],
552 [ 'rc_user' => 'actormigration_user' ],
555 [ 'actormigration' => 'join' ],
558 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
560 [ 'commentstore' => 'table' ],
561 [ 'commentstore' => 'field' ],
564 [ 'commentstore' => 'join' ],
567 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
570 [ 'rc_patrolled', 'rc_log_type' ],
576 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
579 [ 'rc_old_len', 'rc_new_len' ],
585 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
588 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
594 [ 'namespaceIds' => [ 0, 1 ] ],
598 [ 'wl_namespace' => [ 0, 1 ] ],
603 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
607 [ 'wl_namespace' => [ 0, 1 ] ],
612 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
616 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
621 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
626 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
630 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
635 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
639 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
643 [ "rc_timestamp <= '20151212010101'" ],
644 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
648 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
652 [ "rc_timestamp >= '20151212010101'" ],
653 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
658 'dir' => WatchedItemQueryService
::DIR_OLDER
,
659 'start' => '20151212020101',
660 'end' => '20151212010101'
665 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
666 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
670 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
674 [ "rc_timestamp >= '20151212010101'" ],
675 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
679 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
683 [ "rc_timestamp <= '20151212010101'" ],
684 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
689 'dir' => WatchedItemQueryService
::DIR_NEWER
,
690 'start' => '20151212010101',
691 'end' => '20151212020101'
696 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
697 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
710 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
719 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
728 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
737 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
746 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
755 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
757 [ 'actormigration' => 'table' ],
759 [ 'actormigration is anon' ],
761 [ 'actormigration' => 'join' ],
764 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
766 [ 'actormigration' => 'table' ],
768 [ 'actormigration is not anon' ],
770 [ 'actormigration' => 'join' ],
773 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
777 [ 'rc_patrolled != 0' ],
782 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
786 [ 'rc_patrolled' => 0 ],
791 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
795 [ 'rc_timestamp >= wl_notificationtimestamp' ],
800 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
804 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
809 [ 'onlyByUser' => 'SomeOtherUser' ],
811 [ 'actormigration' => 'table' ],
813 [ 'actormigration_conds' ],
815 [ 'actormigration' => 'join' ],
818 [ 'notByUser' => 'SomeOtherUser' ],
820 [ 'actormigration' => 'table' ],
822 [ 'NOT(actormigration_conds)' ],
824 [ 'actormigration' => 'join' ],
827 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
828 [ '20151212010101', 123 ],
832 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
834 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
838 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
839 [ '20151212010101', 123 ],
843 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
845 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
849 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
850 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
854 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
856 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
863 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
865 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
868 array $expectedExtraTables,
869 array $expectedExtraFields,
870 array $expectedExtraConds,
871 array $expectedDbOptions,
872 array $expectedExtraJoinConds
874 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
875 $expectedFields = array_merge(
883 'wl_notificationtimestamp',
891 $expectedConds = array_merge(
892 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
895 $expectedJoinConds = array_merge(
900 'wl_namespace=rc_namespace',
909 $expectedExtraJoinConds
912 $mockDb = $this->getMockDb();
913 $mockDb->expects( $this->once() )
919 $this->isType( 'string' ),
923 ->will( $this->returnValue( [] ) );
925 $queryService = $this->newService( $mockDb );
926 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
928 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
930 $this->assertEmpty( $items );
931 $this->assertNull( $startFrom );
934 public function filterPatrolledOptionProvider() {
936 [ WatchedItemQueryService
::FILTER_PATROLLED
],
937 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
942 * @dataProvider filterPatrolledOptionProvider
944 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
947 $mockDb = $this->getMockDb();
948 $mockDb->expects( $this->once() )
951 [ 'recentchanges', 'watchlist', 'page' ],
952 $this->isType( 'array' ),
953 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
954 $this->isType( 'string' ),
955 $this->isType( 'array' ),
956 $this->isType( 'array' )
958 ->will( $this->returnValue( [] ) );
960 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
962 $queryService = $this->newService( $mockDb );
963 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
965 [ 'filters' => [ $filtersOption ] ]
968 $this->assertEmpty( $items );
971 public function mysqlIndexOptimizationProvider() {
976 [ "rc_timestamp > ''" ],
980 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
981 [ "rc_timestamp <= '20151212010101'" ],
985 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
986 [ "rc_timestamp >= '20151212010101'" ],
997 * @dataProvider mysqlIndexOptimizationProvider
999 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1002 array $expectedExtraConds
1004 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1005 $conds = array_merge( $commonConds, $expectedExtraConds );
1007 $mockDb = $this->getMockDb();
1008 $mockDb->expects( $this->once() )
1009 ->method( 'select' )
1011 [ 'recentchanges', 'watchlist', 'page' ],
1012 $this->isType( 'array' ),
1014 $this->isType( 'string' ),
1015 $this->isType( 'array' ),
1016 $this->isType( 'array' )
1018 ->will( $this->returnValue( [] ) );
1019 $mockDb->expects( $this->any() )
1020 ->method( 'getType' )
1021 ->will( $this->returnValue( $dbType ) );
1023 $queryService = $this->newService( $mockDb );
1024 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1026 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1028 $this->assertEmpty( $items );
1031 public function userPermissionRelatedExtraChecksProvider() {
1038 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1039 LogPage
::DELETED_ACTION
. ')'
1048 '(rc_type != ' . RC_LOG
. ') OR (' .
1049 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1050 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1059 '(rc_type != ' . RC_LOG
. ') OR (' .
1060 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1061 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1066 [ 'onlyByUser' => 'SomeOtherUser' ],
1068 [ 'actormigration' => 'table' ],
1070 'actormigration_conds',
1071 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1072 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1073 LogPage
::DELETED_ACTION
. ')'
1075 [ 'actormigration' => 'join' ],
1078 [ 'onlyByUser' => 'SomeOtherUser' ],
1080 [ 'actormigration' => 'table' ],
1082 'actormigration_conds',
1083 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1084 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1085 '(rc_type != ' . RC_LOG
. ') OR (' .
1086 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1087 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1089 [ 'actormigration' => 'join' ],
1092 [ 'onlyByUser' => 'SomeOtherUser' ],
1094 [ 'actormigration' => 'table' ],
1096 'actormigration_conds',
1097 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1098 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1099 '(rc_type != ' . RC_LOG
. ') OR (' .
1100 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1101 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1103 [ 'actormigration' => 'join' ],
1109 * @dataProvider userPermissionRelatedExtraChecksProvider
1111 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1114 array $expectedExtraTables,
1115 array $expectedExtraConds,
1116 array $expectedExtraJoins
1118 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1119 $conds = array_merge( $commonConds, $expectedExtraConds );
1121 $mockDb = $this->getMockDb();
1122 $mockDb->expects( $this->once() )
1123 ->method( 'select' )
1125 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1126 $this->isType( 'array' ),
1128 $this->isType( 'string' ),
1129 $this->isType( 'array' ),
1131 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1132 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1133 ], $expectedExtraJoins )
1135 ->will( $this->returnValue( [] ) );
1137 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1139 $queryService = $this->newService( $mockDb );
1140 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1142 $this->assertEmpty( $items );
1145 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1146 $mockDb = $this->getMockDb();
1147 $mockDb->expects( $this->once() )
1148 ->method( 'select' )
1150 [ 'recentchanges', 'watchlist' ],
1158 'wl_notificationtimestamp',
1164 [ 'wl_user' => 1, ],
1165 $this->isType( 'string' ),
1171 'wl_namespace=rc_namespace',
1177 ->will( $this->returnValue( [] ) );
1179 $queryService = $this->newService( $mockDb );
1180 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1182 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1184 $this->assertEmpty( $items );
1187 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1190 [ 'rcTypes' => [ 1337 ] ],
1192 'Bad value for parameter $options[\'rcTypes\']',
1195 [ 'rcTypes' => [ 'edit' ] ],
1197 'Bad value for parameter $options[\'rcTypes\']',
1200 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1202 'Bad value for parameter $options[\'rcTypes\']',
1207 'Bad value for parameter $options[\'dir\']',
1210 [ 'start' => '20151212010101' ],
1212 'Bad value for parameter $options[\'dir\']: must be provided',
1215 [ 'end' => '20151212010101' ],
1217 'Bad value for parameter $options[\'dir\']: must be provided',
1221 [ '20151212010101', 123 ],
1222 'Bad value for parameter $options[\'dir\']: must be provided',
1225 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1227 'Bad value for parameter $startFrom: must be a two-element array',
1230 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1231 [ '20151212010101' ],
1232 'Bad value for parameter $startFrom: must be a two-element array',
1235 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1236 [ '20151212010101', 123, 'foo' ],
1237 'Bad value for parameter $startFrom: must be a two-element array',
1240 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1242 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1245 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1247 'Bad value for parameter $options[\'watchlistOwner\']',
1253 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1255 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1258 $expectedInExceptionMessage
1260 $mockDb = $this->getMockDb();
1261 $mockDb->expects( $this->never() )
1262 ->method( $this->anything() );
1264 $queryService = $this->newService( $mockDb );
1265 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1267 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1268 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1271 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1272 $mockDb = $this->getMockDb();
1273 $mockDb->expects( $this->once() )
1274 ->method( 'select' )
1276 [ 'recentchanges', 'watchlist', 'page' ],
1284 'wl_notificationtimestamp',
1287 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1288 $this->isType( 'string' ),
1294 'wl_namespace=rc_namespace',
1300 'rc_cur_id=page_id',
1304 ->will( $this->returnValue( [] ) );
1306 $queryService = $this->newService( $mockDb );
1307 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1309 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1311 [ 'usedInGenerator' => true ]
1314 $this->assertEmpty( $items );
1317 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1318 $mockDb = $this->getMockDb();
1319 $mockDb->expects( $this->once() )
1320 ->method( 'select' )
1322 [ 'recentchanges', 'watchlist' ],
1330 'wl_notificationtimestamp',
1334 $this->isType( 'string' ),
1340 'wl_namespace=rc_namespace',
1346 ->will( $this->returnValue( [] ) );
1348 $queryService = $this->newService( $mockDb );
1349 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1351 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1353 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1356 $this->assertEmpty( $items );
1359 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1360 $mockDb = $this->getMockDb();
1361 $mockDb->expects( $this->once() )
1362 ->method( 'select' )
1364 $this->isType( 'array' ),
1365 $this->isType( 'array' ),
1368 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1370 $this->isType( 'string' ),
1371 $this->isType( 'array' ),
1372 $this->isType( 'array' )
1374 ->will( $this->returnValue( [] ) );
1376 $queryService = $this->newService( $mockDb );
1377 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1378 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1379 $otherUser->expects( $this->once() )
1380 ->method( 'getOption' )
1381 ->with( 'watchlisttoken' )
1382 ->willReturn( '0123456789abcdef' );
1384 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1386 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1389 $this->assertEmpty( $items );
1392 public function invalidWatchlistTokenProvider() {
1400 * @dataProvider invalidWatchlistTokenProvider
1402 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1403 $mockDb = $this->getMockDb();
1404 $mockDb->expects( $this->never() )
1405 ->method( $this->anything() );
1407 $queryService = $this->newService( $mockDb );
1408 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1409 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1410 $otherUser->expects( $this->once() )
1411 ->method( 'getOption' )
1412 ->with( 'watchlisttoken' )
1413 ->willReturn( '0123456789abcdef' );
1415 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1416 $queryService->getWatchedItemsWithRecentChangeInfo(
1418 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1422 public function testGetWatchedItemsForUser() {
1423 $mockDb = $this->getMockDb();
1424 $mockDb->expects( $this->once() )
1425 ->method( 'select' )
1428 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1431 ->will( $this->returnValue( [
1432 $this->getFakeRow( [
1433 'wl_namespace' => 0,
1434 'wl_title' => 'Foo1',
1435 'wl_notificationtimestamp' => '20151212010101',
1437 $this->getFakeRow( [
1438 'wl_namespace' => 1,
1439 'wl_title' => 'Foo2',
1440 'wl_notificationtimestamp' => null,
1444 $queryService = $this->newService( $mockDb );
1445 $user = $this->getMockNonAnonUserWithId( 1 );
1447 $items = $queryService->getWatchedItemsForUser( $user );
1449 $this->assertInternalType( 'array', $items );
1450 $this->assertCount( 2, $items );
1451 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1452 $this->assertEquals(
1453 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1456 $this->assertEquals(
1457 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1462 public function provideGetWatchedItemsForUserOptions() {
1465 [ 'namespaceIds' => [ 0, 1 ], ],
1466 [ 'wl_namespace' => [ 0, 1 ], ],
1470 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1472 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1476 'namespaceIds' => [ 0 ],
1477 'sort' => WatchedItemQueryService
::SORT_ASC
,
1479 [ 'wl_namespace' => [ 0 ], ],
1480 [ 'ORDER BY' => 'wl_title ASC' ]
1489 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1490 'limit' => "10; DROP TABLE watchlist;\n--",
1492 [ 'wl_namespace' => [ 0, 1 ], ],
1496 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1497 [ 'wl_notificationtimestamp IS NOT NULL' ],
1501 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1502 [ 'wl_notificationtimestamp IS NULL' ],
1506 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1508 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1512 'namespaceIds' => [ 0 ],
1513 'sort' => WatchedItemQueryService
::SORT_DESC
,
1515 [ 'wl_namespace' => [ 0 ], ],
1516 [ 'ORDER BY' => 'wl_title DESC' ]
1522 * @dataProvider provideGetWatchedItemsForUserOptions
1524 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1526 array $expectedConds,
1527 array $expectedDbOptions
1529 $mockDb = $this->getMockDb();
1530 $user = $this->getMockNonAnonUserWithId( 1 );
1532 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1533 $mockDb->expects( $this->once() )
1534 ->method( 'select' )
1537 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1539 $this->isType( 'string' ),
1542 ->will( $this->returnValue( [] ) );
1544 $queryService = $this->newService( $mockDb );
1546 $items = $queryService->getWatchedItemsForUser( $user, $options );
1547 $this->assertEmpty( $items );
1550 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1554 'from' => new TitleValue( 0, 'SomeDbKey' ),
1555 'sort' => WatchedItemQueryService
::SORT_ASC
1557 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1558 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1562 'from' => new TitleValue( 0, 'SomeDbKey' ),
1563 'sort' => WatchedItemQueryService
::SORT_DESC
,
1565 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1566 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1570 'until' => new TitleValue( 0, 'SomeDbKey' ),
1571 'sort' => WatchedItemQueryService
::SORT_ASC
1573 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1574 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1578 'until' => new TitleValue( 0, 'SomeDbKey' ),
1579 'sort' => WatchedItemQueryService
::SORT_DESC
1581 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1582 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1586 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1587 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1588 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1589 'sort' => WatchedItemQueryService
::SORT_ASC
1592 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1593 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1594 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1596 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1600 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1601 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1602 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1603 'sort' => WatchedItemQueryService
::SORT_DESC
1606 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1607 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1608 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1610 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1616 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1618 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1620 array $expectedConds,
1621 array $expectedDbOptions
1623 $user = $this->getMockNonAnonUserWithId( 1 );
1625 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1627 $mockDb = $this->getMockDb();
1628 $mockDb->expects( $this->any() )
1629 ->method( 'addQuotes' )
1630 ->will( $this->returnCallback( function ( $value ) {
1633 $mockDb->expects( $this->any() )
1634 ->method( 'makeList' )
1636 $this->isType( 'array' ),
1637 $this->isType( 'int' )
1639 ->will( $this->returnCallback( function ( $a, $conj ) {
1640 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1641 return implode( $sqlConj, array_map( function ( $s ) {
1642 return '(' . $s . ')';
1646 $mockDb->expects( $this->once() )
1647 ->method( 'select' )
1650 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1652 $this->isType( 'string' ),
1655 ->will( $this->returnValue( [] ) );
1657 $queryService = $this->newService( $mockDb );
1659 $items = $queryService->getWatchedItemsForUser( $user, $options );
1660 $this->assertEmpty( $items );
1663 public function getWatchedItemsForUserInvalidOptionsProvider() {
1666 [ 'sort' => 'foo' ],
1667 'Bad value for parameter $options[\'sort\']'
1670 [ 'filter' => 'foo' ],
1671 'Bad value for parameter $options[\'filter\']'
1674 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1675 'Bad value for parameter $options[\'sort\']: must be provided'
1678 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1679 'Bad value for parameter $options[\'sort\']: must be provided'
1682 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1683 'Bad value for parameter $options[\'sort\']: must be provided'
1689 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1691 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1693 $expectedInExceptionMessage
1695 $queryService = $this->newService( $this->getMockDb() );
1697 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1698 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1701 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1702 $mockDb = $this->getMockDb();
1704 $mockDb->expects( $this->never() )
1705 ->method( $this->anything() );
1707 $queryService = $this->newService( $mockDb );
1709 $items = $queryService->getWatchedItemsForUser(
1710 new UserIdentityValue( 0, 'AnonUser', 0 ) );
1711 $this->assertEmpty( $items );