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
{
14 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
16 private function getMockCommentStore() {
17 $mockStore = $this->getMockBuilder( CommentStore
::class )
18 ->disableOriginalConstructor()
20 $mockStore->expects( $this->any() )
21 ->method( 'getFields' )
22 ->willReturn( [ 'commentstore' => 'fields' ] );
23 $mockStore->expects( $this->any() )
26 'tables' => [ 'commentstore' => 'table' ],
27 'fields' => [ 'commentstore' => 'field' ],
28 'joins' => [ 'commentstore' => 'join' ],
34 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
36 private function getMockActorMigration() {
37 $mockStore = $this->getMockBuilder( ActorMigration
::class )
38 ->disableOriginalConstructor()
40 $mockStore->expects( $this->any() )
43 'tables' => [ 'actormigration' => 'table' ],
45 'rc_user' => 'actormigration_user',
46 'rc_user_text' => 'actormigration_user_text',
47 'rc_actor' => 'actormigration_actor',
49 'joins' => [ 'actormigration' => 'join' ],
51 $mockStore->expects( $this->any() )
52 ->method( 'getWhere' )
54 'tables' => [ 'actormigration' => 'table' ],
55 'conds' => 'actormigration_conds',
56 'joins' => [ 'actormigration' => 'join' ],
58 $mockStore->expects( $this->any() )
60 ->willReturn( 'actormigration is anon' );
61 $mockStore->expects( $this->any() )
62 ->method( 'isNotAnon' )
63 ->willReturn( 'actormigration is not anon' );
68 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
69 * @return WatchedItemQueryService
71 private function newService( $mockDb ) {
72 return new WatchedItemQueryService(
73 $this->getMockLoadBalancer( $mockDb ),
74 $this->getMockCommentStore(),
75 $this->getMockActorMigration(),
76 $this->getMockWatchedItemStore()
81 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
83 private function getMockDb() {
84 $mock = $this->createMock( IDatabase
::class );
86 $mock->expects( $this->any() )
87 ->method( 'makeList' )
89 $this->isType( 'array' ),
90 $this->isType( 'int' )
92 ->will( $this->returnCallback( function ( $a, $conj ) {
93 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
95 foreach ( $a as $k => $v ) {
98 } elseif ( is_array( $v ) ) {
99 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
101 $conds[] = "($k = '$v')";
104 return implode( $sqlConj, $conds );
107 $mock->expects( $this->any() )
108 ->method( 'addQuotes' )
109 ->will( $this->returnCallback( function ( $value ) {
113 $mock->expects( $this->any() )
114 ->method( 'timestamp' )
115 ->will( $this->returnArgument( 0 ) );
117 $mock->expects( $this->any() )
119 ->willReturnCallback( function ( $a, $b ) {
127 * @param PHPUnit_Framework_MockObject_MockObject|IDatabase $mockDb
128 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
130 private function getMockLoadBalancer( $mockDb ) {
131 $mock = $this->getMockBuilder( LoadBalancer
::class )
132 ->disableOriginalConstructor()
134 $mock->expects( $this->any() )
135 ->method( 'getConnectionRef' )
137 ->will( $this->returnValue( $mockDb ) );
142 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
144 private function getMockWatchedItemStore() {
145 $mock = $this->getMockBuilder( WatchedItemStore
::class )
146 ->disableOriginalConstructor()
148 $mock->expects( $this->any() )
149 ->method( 'getLatestNotificationTimestamp' )
150 ->will( $this->returnCallback( function ( $timestamp ) {
158 * @param string[] $extraMethods Extra methods that are expected might be called
159 * @return PHPUnit_Framework_MockObject_MockObject|User
161 private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
162 $mock = $this->getMockBuilder( User
::class )->getMock();
163 $mock->method( 'isRegistered' )->willReturn( true );
164 $mock->method( 'getId' )->willReturn( $id );
165 $methods = array_merge( [
169 $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) );
175 * @param string[] $extraMethods Extra methods that are expected might be called
176 * @return PHPUnit_Framework_MockObject_MockObject|User
178 private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
179 $mock = $this->getMockNonAnonUserWithId( $id,
180 array_merge( [ 'isAllowed', 'isAllowedAny', 'useRCPatrol' ], $extraMethods ) );
181 $mock->method( 'isAllowed' )->willReturn( true );
182 $mock->method( 'isAllowedAny' )->willReturn( true );
183 $mock->method( 'useRCPatrol' )->willReturn( true );
189 * @param string $notAllowedAction
190 * @return PHPUnit_Framework_MockObject_MockObject|User
192 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
193 $mock = $this->getMockNonAnonUserWithId( $id,
194 [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
196 $mock->method( 'isAllowed' )
197 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
198 return $action !== $notAllowedAction;
200 $mock->method( 'isAllowedAny' )
201 ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
202 return !in_array( $notAllowedAction, $actions );
204 $mock->method( 'useRCPatrol' )->willReturn( false );
205 $mock->method( 'useNPPatrol' )->willReturn( false );
212 * @return PHPUnit_Framework_MockObject_MockObject|User
214 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
215 $mock = $this->getMockNonAnonUserWithId( $id,
216 [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
218 $mock->expects( $this->any() )
219 ->method( 'isAllowed' )
220 ->will( $this->returnValue( true ) );
221 $mock->expects( $this->any() )
222 ->method( 'isAllowedAny' )
223 ->will( $this->returnValue( true ) );
225 $mock->expects( $this->any() )
226 ->method( 'useRCPatrol' )
227 ->will( $this->returnValue( false ) );
228 $mock->expects( $this->any() )
229 ->method( 'useNPPatrol' )
230 ->will( $this->returnValue( false ) );
235 private function getFakeRow( array $rowValues ) {
236 $fakeRow = new stdClass();
237 foreach ( $rowValues as $valueName => $value ) {
238 $fakeRow->$valueName = $value;
243 public function testGetWatchedItemsWithRecentChangeInfo() {
244 $mockDb = $this->getMockDb();
245 $mockDb->expects( $this->once() )
248 [ 'recentchanges', 'watchlist', 'page' ],
256 'wl_notificationtimestamp',
263 '(rc_this_oldid=page_latest) OR (rc_type=3)',
265 $this->isType( 'string' ),
273 'wl_namespace=rc_namespace',
283 ->will( $this->returnValue( [
287 'rc_title' => 'Foo1',
288 'rc_timestamp' => '20151212010101',
291 'wl_notificationtimestamp' => '20151212010101',
296 'rc_title' => 'Foo2',
297 'rc_timestamp' => '20151212010102',
300 'wl_notificationtimestamp' => null,
305 'rc_title' => 'Foo3',
306 'rc_timestamp' => '20151212010103',
309 'wl_notificationtimestamp' => null,
313 $queryService = $this->newService( $mockDb );
314 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
317 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
318 $user, [ 'limit' => 2 ], $startFrom
321 $this->assertInternalType( 'array', $items );
322 $this->assertCount( 2, $items );
324 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
325 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
326 $this->assertInternalType( 'array', $recentChangeInfo );
330 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
337 'rc_title' => 'Foo1',
338 'rc_timestamp' => '20151212010101',
346 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
353 'rc_title' => 'Foo2',
354 'rc_timestamp' => '20151212010102',
361 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
364 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
365 $mockDb = $this->getMockDb();
366 $mockDb->expects( $this->once() )
369 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
377 'wl_notificationtimestamp',
381 'extension_dummy_field',
385 '(rc_this_oldid=page_latest) OR (rc_type=3)',
386 'extension_dummy_cond',
388 $this->isType( 'string' ),
390 'extension_dummy_option',
396 'wl_namespace=rc_namespace',
404 'extension_dummy_join_cond' => [],
407 ->will( $this->returnValue( [
411 'rc_title' => 'Foo1',
412 'rc_timestamp' => '20151212010101',
415 'wl_notificationtimestamp' => '20151212010101',
420 'rc_title' => 'Foo2',
421 'rc_timestamp' => '20151212010102',
424 'wl_notificationtimestamp' => null,
428 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
430 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
432 $mockExtension->expects( $this->once() )
433 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
435 $this->identicalTo( $user ),
436 $this->isType( 'array' ),
437 $this->isInstanceOf( IDatabase
::class ),
438 $this->isType( 'array' ),
439 $this->isType( 'array' ),
440 $this->isType( 'array' ),
441 $this->isType( 'array' ),
442 $this->isType( 'array' )
444 ->will( $this->returnCallback( function (
445 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
447 $tables[] = 'extension_dummy_table';
448 $fields[] = 'extension_dummy_field';
449 $conds[] = 'extension_dummy_cond';
450 $dbOptions[] = 'extension_dummy_option';
451 $joinConds['extension_dummy_join_cond'] = [];
453 $mockExtension->expects( $this->once() )
454 ->method( 'modifyWatchedItemsWithRCInfo' )
456 $this->identicalTo( $user ),
457 $this->isType( 'array' ),
458 $this->isInstanceOf( IDatabase
::class ),
459 $this->isType( 'array' ),
461 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
463 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
464 foreach ( $items as $i => &$item ) {
465 $item[1]['extension_dummy_field'] = $i;
469 $this->assertNull( $startFrom );
470 $startFrom = [ '20160203123456', 42 ];
473 $queryService = $this->newService( $mockDb );
474 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
477 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
478 $user, [], $startFrom
481 $this->assertInternalType( 'array', $items );
482 $this->assertCount( 2, $items );
484 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
485 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
486 $this->assertInternalType( 'array', $recentChangeInfo );
490 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
497 'rc_title' => 'Foo1',
498 'rc_timestamp' => '20151212010101',
501 'extension_dummy_field' => 0,
507 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
514 'rc_title' => 'Foo2',
515 'rc_timestamp' => '20151212010102',
518 'extension_dummy_field' => 1,
523 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
526 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
529 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
532 [ 'rc_type', 'rc_minor', 'rc_bot' ],
538 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
540 [ 'actormigration' => 'table' ],
541 [ 'rc_user_text' => 'actormigration_user_text' ],
544 [ 'actormigration' => 'join' ],
547 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
549 [ 'actormigration' => 'table' ],
550 [ 'rc_user' => 'actormigration_user' ],
553 [ 'actormigration' => 'join' ],
556 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
558 [ 'commentstore' => 'table' ],
559 [ 'commentstore' => 'field' ],
562 [ 'commentstore' => 'join' ],
565 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
568 [ 'rc_patrolled', 'rc_log_type' ],
574 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
577 [ 'rc_old_len', 'rc_new_len' ],
583 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
586 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
592 [ 'namespaceIds' => [ 0, 1 ] ],
596 [ 'wl_namespace' => [ 0, 1 ] ],
601 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
605 [ 'wl_namespace' => [ 0, 1 ] ],
610 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
614 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
619 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
624 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
628 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
633 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
637 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
641 [ "rc_timestamp <= '20151212010101'" ],
642 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
646 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
650 [ "rc_timestamp >= '20151212010101'" ],
651 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
656 'dir' => WatchedItemQueryService
::DIR_OLDER
,
657 'start' => '20151212020101',
658 'end' => '20151212010101'
663 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
664 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
668 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
672 [ "rc_timestamp >= '20151212010101'" ],
673 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
677 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
681 [ "rc_timestamp <= '20151212010101'" ],
682 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
687 'dir' => WatchedItemQueryService
::DIR_NEWER
,
688 'start' => '20151212010101',
689 'end' => '20151212020101'
694 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
695 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
708 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
717 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
726 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
735 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
744 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
753 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
755 [ 'actormigration' => 'table' ],
757 [ 'actormigration is anon' ],
759 [ 'actormigration' => 'join' ],
762 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
764 [ 'actormigration' => 'table' ],
766 [ 'actormigration is not anon' ],
768 [ 'actormigration' => 'join' ],
771 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
775 [ 'rc_patrolled != 0' ],
780 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
784 [ 'rc_patrolled' => 0 ],
789 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
793 [ 'rc_timestamp >= wl_notificationtimestamp' ],
798 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
802 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
807 [ 'onlyByUser' => 'SomeOtherUser' ],
809 [ 'actormigration' => 'table' ],
811 [ 'actormigration_conds' ],
813 [ 'actormigration' => 'join' ],
816 [ 'notByUser' => 'SomeOtherUser' ],
818 [ 'actormigration' => 'table' ],
820 [ 'NOT(actormigration_conds)' ],
822 [ 'actormigration' => 'join' ],
825 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
826 [ '20151212010101', 123 ],
830 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
832 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
836 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
837 [ '20151212010101', 123 ],
841 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
843 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
847 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
848 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
852 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
854 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
861 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
863 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
866 array $expectedExtraTables,
867 array $expectedExtraFields,
868 array $expectedExtraConds,
869 array $expectedDbOptions,
870 array $expectedExtraJoinConds
872 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
873 $expectedFields = array_merge(
881 'wl_notificationtimestamp',
889 $expectedConds = array_merge(
890 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
893 $expectedJoinConds = array_merge(
898 'wl_namespace=rc_namespace',
907 $expectedExtraJoinConds
910 $mockDb = $this->getMockDb();
911 $mockDb->expects( $this->once() )
917 $this->isType( 'string' ),
921 ->will( $this->returnValue( [] ) );
923 $queryService = $this->newService( $mockDb );
924 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
926 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
928 $this->assertEmpty( $items );
929 $this->assertNull( $startFrom );
932 public function filterPatrolledOptionProvider() {
934 [ WatchedItemQueryService
::FILTER_PATROLLED
],
935 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
940 * @dataProvider filterPatrolledOptionProvider
942 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
945 $mockDb = $this->getMockDb();
946 $mockDb->expects( $this->once() )
949 [ 'recentchanges', 'watchlist', 'page' ],
950 $this->isType( 'array' ),
951 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
952 $this->isType( 'string' ),
953 $this->isType( 'array' ),
954 $this->isType( 'array' )
956 ->will( $this->returnValue( [] ) );
958 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
960 $queryService = $this->newService( $mockDb );
961 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
963 [ 'filters' => [ $filtersOption ] ]
966 $this->assertEmpty( $items );
969 public function mysqlIndexOptimizationProvider() {
974 [ "rc_timestamp > ''" ],
978 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
979 [ "rc_timestamp <= '20151212010101'" ],
983 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
984 [ "rc_timestamp >= '20151212010101'" ],
995 * @dataProvider mysqlIndexOptimizationProvider
997 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1000 array $expectedExtraConds
1002 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1003 $conds = array_merge( $commonConds, $expectedExtraConds );
1005 $mockDb = $this->getMockDb();
1006 $mockDb->expects( $this->once() )
1007 ->method( 'select' )
1009 [ 'recentchanges', 'watchlist', 'page' ],
1010 $this->isType( 'array' ),
1012 $this->isType( 'string' ),
1013 $this->isType( 'array' ),
1014 $this->isType( 'array' )
1016 ->will( $this->returnValue( [] ) );
1017 $mockDb->expects( $this->any() )
1018 ->method( 'getType' )
1019 ->will( $this->returnValue( $dbType ) );
1021 $queryService = $this->newService( $mockDb );
1022 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1024 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1026 $this->assertEmpty( $items );
1029 public function userPermissionRelatedExtraChecksProvider() {
1036 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1037 LogPage
::DELETED_ACTION
. ')'
1046 '(rc_type != ' . RC_LOG
. ') OR (' .
1047 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1048 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1057 '(rc_type != ' . RC_LOG
. ') OR (' .
1058 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1059 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1064 [ 'onlyByUser' => 'SomeOtherUser' ],
1066 [ 'actormigration' => 'table' ],
1068 'actormigration_conds',
1069 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1070 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1071 LogPage
::DELETED_ACTION
. ')'
1073 [ 'actormigration' => 'join' ],
1076 [ 'onlyByUser' => 'SomeOtherUser' ],
1078 [ 'actormigration' => 'table' ],
1080 'actormigration_conds',
1081 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1082 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1083 '(rc_type != ' . RC_LOG
. ') OR (' .
1084 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1085 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1087 [ 'actormigration' => 'join' ],
1090 [ 'onlyByUser' => 'SomeOtherUser' ],
1092 [ 'actormigration' => 'table' ],
1094 'actormigration_conds',
1095 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1096 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1097 '(rc_type != ' . RC_LOG
. ') OR (' .
1098 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1099 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1101 [ 'actormigration' => 'join' ],
1107 * @dataProvider userPermissionRelatedExtraChecksProvider
1109 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1112 array $expectedExtraTables,
1113 array $expectedExtraConds,
1114 array $expectedExtraJoins
1116 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1117 $conds = array_merge( $commonConds, $expectedExtraConds );
1119 $mockDb = $this->getMockDb();
1120 $mockDb->expects( $this->once() )
1121 ->method( 'select' )
1123 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1124 $this->isType( 'array' ),
1126 $this->isType( 'string' ),
1127 $this->isType( 'array' ),
1129 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1130 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1131 ], $expectedExtraJoins )
1133 ->will( $this->returnValue( [] ) );
1135 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1137 $queryService = $this->newService( $mockDb );
1138 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1140 $this->assertEmpty( $items );
1143 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1144 $mockDb = $this->getMockDb();
1145 $mockDb->expects( $this->once() )
1146 ->method( 'select' )
1148 [ 'recentchanges', 'watchlist' ],
1156 'wl_notificationtimestamp',
1162 [ 'wl_user' => 1, ],
1163 $this->isType( 'string' ),
1169 'wl_namespace=rc_namespace',
1175 ->will( $this->returnValue( [] ) );
1177 $queryService = $this->newService( $mockDb );
1178 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1180 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1182 $this->assertEmpty( $items );
1185 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1188 [ 'rcTypes' => [ 1337 ] ],
1190 'Bad value for parameter $options[\'rcTypes\']',
1193 [ 'rcTypes' => [ 'edit' ] ],
1195 'Bad value for parameter $options[\'rcTypes\']',
1198 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1200 'Bad value for parameter $options[\'rcTypes\']',
1205 'Bad value for parameter $options[\'dir\']',
1208 [ 'start' => '20151212010101' ],
1210 'Bad value for parameter $options[\'dir\']: must be provided',
1213 [ 'end' => '20151212010101' ],
1215 'Bad value for parameter $options[\'dir\']: must be provided',
1219 [ '20151212010101', 123 ],
1220 'Bad value for parameter $options[\'dir\']: must be provided',
1223 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1225 'Bad value for parameter $startFrom: must be a two-element array',
1228 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1229 [ '20151212010101' ],
1230 'Bad value for parameter $startFrom: must be a two-element array',
1233 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1234 [ '20151212010101', 123, 'foo' ],
1235 'Bad value for parameter $startFrom: must be a two-element array',
1238 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1240 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1243 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1245 'Bad value for parameter $options[\'watchlistOwner\']',
1251 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1253 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1256 $expectedInExceptionMessage
1258 $mockDb = $this->getMockDb();
1259 $mockDb->expects( $this->never() )
1260 ->method( $this->anything() );
1262 $queryService = $this->newService( $mockDb );
1263 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1265 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1266 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1269 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1270 $mockDb = $this->getMockDb();
1271 $mockDb->expects( $this->once() )
1272 ->method( 'select' )
1274 [ 'recentchanges', 'watchlist', 'page' ],
1282 'wl_notificationtimestamp',
1285 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1286 $this->isType( 'string' ),
1292 'wl_namespace=rc_namespace',
1298 'rc_cur_id=page_id',
1302 ->will( $this->returnValue( [] ) );
1304 $queryService = $this->newService( $mockDb );
1305 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1307 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1309 [ 'usedInGenerator' => true ]
1312 $this->assertEmpty( $items );
1315 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1316 $mockDb = $this->getMockDb();
1317 $mockDb->expects( $this->once() )
1318 ->method( 'select' )
1320 [ 'recentchanges', 'watchlist' ],
1328 'wl_notificationtimestamp',
1332 $this->isType( 'string' ),
1338 'wl_namespace=rc_namespace',
1344 ->will( $this->returnValue( [] ) );
1346 $queryService = $this->newService( $mockDb );
1347 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1349 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1351 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1354 $this->assertEmpty( $items );
1357 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1358 $mockDb = $this->getMockDb();
1359 $mockDb->expects( $this->once() )
1360 ->method( 'select' )
1362 $this->isType( 'array' ),
1363 $this->isType( 'array' ),
1366 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1368 $this->isType( 'string' ),
1369 $this->isType( 'array' ),
1370 $this->isType( 'array' )
1372 ->will( $this->returnValue( [] ) );
1374 $queryService = $this->newService( $mockDb );
1375 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1376 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1377 $otherUser->expects( $this->once() )
1378 ->method( 'getOption' )
1379 ->with( 'watchlisttoken' )
1380 ->willReturn( '0123456789abcdef' );
1382 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1384 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1387 $this->assertEmpty( $items );
1390 public function invalidWatchlistTokenProvider() {
1398 * @dataProvider invalidWatchlistTokenProvider
1400 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1401 $mockDb = $this->getMockDb();
1402 $mockDb->expects( $this->never() )
1403 ->method( $this->anything() );
1405 $queryService = $this->newService( $mockDb );
1406 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1407 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1408 $otherUser->expects( $this->once() )
1409 ->method( 'getOption' )
1410 ->with( 'watchlisttoken' )
1411 ->willReturn( '0123456789abcdef' );
1413 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1414 $queryService->getWatchedItemsWithRecentChangeInfo(
1416 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1420 public function testGetWatchedItemsForUser() {
1421 $mockDb = $this->getMockDb();
1422 $mockDb->expects( $this->once() )
1423 ->method( 'select' )
1426 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1429 ->will( $this->returnValue( [
1430 $this->getFakeRow( [
1431 'wl_namespace' => 0,
1432 'wl_title' => 'Foo1',
1433 'wl_notificationtimestamp' => '20151212010101',
1435 $this->getFakeRow( [
1436 'wl_namespace' => 1,
1437 'wl_title' => 'Foo2',
1438 'wl_notificationtimestamp' => null,
1442 $queryService = $this->newService( $mockDb );
1443 $user = $this->getMockNonAnonUserWithId( 1 );
1445 $items = $queryService->getWatchedItemsForUser( $user );
1447 $this->assertInternalType( 'array', $items );
1448 $this->assertCount( 2, $items );
1449 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1450 $this->assertEquals(
1451 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1454 $this->assertEquals(
1455 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1460 public function provideGetWatchedItemsForUserOptions() {
1463 [ 'namespaceIds' => [ 0, 1 ], ],
1464 [ 'wl_namespace' => [ 0, 1 ], ],
1468 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1470 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1474 'namespaceIds' => [ 0 ],
1475 'sort' => WatchedItemQueryService
::SORT_ASC
,
1477 [ 'wl_namespace' => [ 0 ], ],
1478 [ 'ORDER BY' => 'wl_title ASC' ]
1487 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1488 'limit' => "10; DROP TABLE watchlist;\n--",
1490 [ 'wl_namespace' => [ 0, 1 ], ],
1494 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1495 [ 'wl_notificationtimestamp IS NOT NULL' ],
1499 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1500 [ 'wl_notificationtimestamp IS NULL' ],
1504 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1506 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1510 'namespaceIds' => [ 0 ],
1511 'sort' => WatchedItemQueryService
::SORT_DESC
,
1513 [ 'wl_namespace' => [ 0 ], ],
1514 [ 'ORDER BY' => 'wl_title DESC' ]
1520 * @dataProvider provideGetWatchedItemsForUserOptions
1522 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1524 array $expectedConds,
1525 array $expectedDbOptions
1527 $mockDb = $this->getMockDb();
1528 $user = $this->getMockNonAnonUserWithId( 1 );
1530 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1531 $mockDb->expects( $this->once() )
1532 ->method( 'select' )
1535 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1537 $this->isType( 'string' ),
1540 ->will( $this->returnValue( [] ) );
1542 $queryService = $this->newService( $mockDb );
1544 $items = $queryService->getWatchedItemsForUser( $user, $options );
1545 $this->assertEmpty( $items );
1548 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1552 'from' => new TitleValue( 0, 'SomeDbKey' ),
1553 'sort' => WatchedItemQueryService
::SORT_ASC
1555 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1556 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1560 'from' => new TitleValue( 0, 'SomeDbKey' ),
1561 'sort' => WatchedItemQueryService
::SORT_DESC
,
1563 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1564 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1568 'until' => new TitleValue( 0, 'SomeDbKey' ),
1569 'sort' => WatchedItemQueryService
::SORT_ASC
1571 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1572 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1576 'until' => new TitleValue( 0, 'SomeDbKey' ),
1577 'sort' => WatchedItemQueryService
::SORT_DESC
1579 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1580 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1584 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1585 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1586 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1587 'sort' => WatchedItemQueryService
::SORT_ASC
1590 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1591 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1592 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1594 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1598 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1599 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1600 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1601 'sort' => WatchedItemQueryService
::SORT_DESC
1604 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1605 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1606 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1608 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1614 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1616 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1618 array $expectedConds,
1619 array $expectedDbOptions
1621 $user = $this->getMockNonAnonUserWithId( 1 );
1623 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1625 $mockDb = $this->getMockDb();
1626 $mockDb->expects( $this->any() )
1627 ->method( 'addQuotes' )
1628 ->will( $this->returnCallback( function ( $value ) {
1631 $mockDb->expects( $this->any() )
1632 ->method( 'makeList' )
1634 $this->isType( 'array' ),
1635 $this->isType( 'int' )
1637 ->will( $this->returnCallback( function ( $a, $conj ) {
1638 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1639 return implode( $sqlConj, array_map( function ( $s ) {
1640 return '(' . $s . ')';
1644 $mockDb->expects( $this->once() )
1645 ->method( 'select' )
1648 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1650 $this->isType( 'string' ),
1653 ->will( $this->returnValue( [] ) );
1655 $queryService = $this->newService( $mockDb );
1657 $items = $queryService->getWatchedItemsForUser( $user, $options );
1658 $this->assertEmpty( $items );
1661 public function getWatchedItemsForUserInvalidOptionsProvider() {
1664 [ 'sort' => 'foo' ],
1665 'Bad value for parameter $options[\'sort\']'
1668 [ 'filter' => 'foo' ],
1669 'Bad value for parameter $options[\'filter\']'
1672 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1673 'Bad value for parameter $options[\'sort\']: must be provided'
1676 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1677 'Bad value for parameter $options[\'sort\']: must be provided'
1680 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1681 'Bad value for parameter $options[\'sort\']: must be provided'
1687 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1689 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1691 $expectedInExceptionMessage
1693 $queryService = $this->newService( $this->getMockDb() );
1695 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1696 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1699 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1700 $mockDb = $this->getMockDb();
1702 $mockDb->expects( $this->never() )
1703 ->method( $this->anything() );
1705 $queryService = $this->newService( $mockDb );
1707 $items = $queryService->getWatchedItemsForUser(
1708 new UserIdentityValue( 0, 'AnonUser', 0 ) );
1709 $this->assertEmpty( $items );