3 use Wikimedia\Rdbms\LoadBalancer
;
4 use Wikimedia\TestingAccessWrapper
;
7 * @covers WatchedItemQueryService
9 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
11 use MediaWikiCoversValidator
;
14 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
16 private function getMockCommentStore() {
17 $mockStore = $this->getMockBuilder( CommentStore
::class )
18 ->disableOriginalConstructor()
20 $mockStore->expects( $this->any() )
21 ->method( 'getFields' )
22 ->willReturn( [ 'commentstore' => 'fields' ] );
23 $mockStore->expects( $this->any() )
26 'tables' => [ 'commentstore' => 'table' ],
27 'fields' => [ 'commentstore' => 'field' ],
28 'joins' => [ 'commentstore' => 'join' ],
34 * @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|Database
83 private function getMockDb() {
84 $mock = $this->getMockBuilder( Database
::class )
85 ->disableOriginalConstructor()
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|Database $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 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
145 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
147 private function getMockWatchedItemStore() {
148 $mock = $this->getMockBuilder( WatchedItemStore
::class )
149 ->disableOriginalConstructor()
151 $mock->expects( $this->any() )
152 ->method( 'getLatestNotificationTimestamp' )
153 ->will( $this->returnCallback( function ( $timestamp ) {
161 * @return PHPUnit_Framework_MockObject_MockObject|User
163 private function getMockNonAnonUserWithId( $id ) {
164 $mock = $this->getMockBuilder( User
::class )->getMock();
165 $mock->expects( $this->any() )
167 ->will( $this->returnValue( false ) );
168 $mock->expects( $this->any() )
170 ->will( $this->returnValue( $id ) );
176 * @return PHPUnit_Framework_MockObject_MockObject|User
178 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
179 $mock = $this->getMockNonAnonUserWithId( $id );
180 $mock->expects( $this->any() )
181 ->method( 'isAllowed' )
182 ->will( $this->returnValue( true ) );
183 $mock->expects( $this->any() )
184 ->method( 'isAllowedAny' )
185 ->will( $this->returnValue( true ) );
186 $mock->expects( $this->any() )
187 ->method( 'useRCPatrol' )
188 ->will( $this->returnValue( true ) );
194 * @param string $notAllowedAction
195 * @return PHPUnit_Framework_MockObject_MockObject|User
197 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
198 $mock = $this->getMockNonAnonUserWithId( $id );
200 $mock->expects( $this->any() )
201 ->method( 'isAllowed' )
202 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
203 return $action !== $notAllowedAction;
205 $mock->expects( $this->any() )
206 ->method( 'isAllowedAny' )
207 ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
208 return !in_array( $notAllowedAction, $actions );
216 * @return PHPUnit_Framework_MockObject_MockObject|User
218 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
219 $mock = $this->getMockNonAnonUserWithId( $id );
221 $mock->expects( $this->any() )
222 ->method( 'isAllowed' )
223 ->will( $this->returnValue( true ) );
224 $mock->expects( $this->any() )
225 ->method( 'isAllowedAny' )
226 ->will( $this->returnValue( true ) );
228 $mock->expects( $this->any() )
229 ->method( 'useRCPatrol' )
230 ->will( $this->returnValue( false ) );
231 $mock->expects( $this->any() )
232 ->method( 'useNPPatrol' )
233 ->will( $this->returnValue( false ) );
238 private function getMockAnonUser() {
239 $mock = $this->getMockBuilder( User
::class )->getMock();
240 $mock->expects( $this->any() )
242 ->will( $this->returnValue( true ) );
246 private function getFakeRow( array $rowValues ) {
247 $fakeRow = new stdClass();
248 foreach ( $rowValues as $valueName => $value ) {
249 $fakeRow->$valueName = $value;
254 public function testGetWatchedItemsWithRecentChangeInfo() {
255 $mockDb = $this->getMockDb();
256 $mockDb->expects( $this->once() )
259 [ 'recentchanges', 'watchlist', 'page' ],
267 'wl_notificationtimestamp',
274 '(rc_this_oldid=page_latest) OR (rc_type=3)',
276 $this->isType( 'string' ),
284 'wl_namespace=rc_namespace',
294 ->will( $this->returnValue( [
298 'rc_title' => 'Foo1',
299 'rc_timestamp' => '20151212010101',
302 'wl_notificationtimestamp' => '20151212010101',
307 'rc_title' => 'Foo2',
308 'rc_timestamp' => '20151212010102',
311 'wl_notificationtimestamp' => null,
316 'rc_title' => 'Foo3',
317 'rc_timestamp' => '20151212010103',
320 'wl_notificationtimestamp' => null,
324 $queryService = $this->newService( $mockDb );
325 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
328 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
329 $user, [ 'limit' => 2 ], $startFrom
332 $this->assertInternalType( 'array', $items );
333 $this->assertCount( 2, $items );
335 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
336 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
337 $this->assertInternalType( 'array', $recentChangeInfo );
341 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
348 'rc_title' => 'Foo1',
349 'rc_timestamp' => '20151212010101',
357 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
364 'rc_title' => 'Foo2',
365 'rc_timestamp' => '20151212010102',
372 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
375 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
376 $mockDb = $this->getMockDb();
377 $mockDb->expects( $this->once() )
380 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
388 'wl_notificationtimestamp',
392 'extension_dummy_field',
396 '(rc_this_oldid=page_latest) OR (rc_type=3)',
397 'extension_dummy_cond',
399 $this->isType( 'string' ),
401 'extension_dummy_option',
407 'wl_namespace=rc_namespace',
415 'extension_dummy_join_cond' => [],
418 ->will( $this->returnValue( [
422 'rc_title' => 'Foo1',
423 'rc_timestamp' => '20151212010101',
426 'wl_notificationtimestamp' => '20151212010101',
431 'rc_title' => 'Foo2',
432 'rc_timestamp' => '20151212010102',
435 'wl_notificationtimestamp' => null,
439 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
441 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
443 $mockExtension->expects( $this->once() )
444 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
446 $this->identicalTo( $user ),
447 $this->isType( 'array' ),
448 $this->isInstanceOf( IDatabase
::class ),
449 $this->isType( 'array' ),
450 $this->isType( 'array' ),
451 $this->isType( 'array' ),
452 $this->isType( 'array' ),
453 $this->isType( 'array' )
455 ->will( $this->returnCallback( function (
456 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
458 $tables[] = 'extension_dummy_table';
459 $fields[] = 'extension_dummy_field';
460 $conds[] = 'extension_dummy_cond';
461 $dbOptions[] = 'extension_dummy_option';
462 $joinConds['extension_dummy_join_cond'] = [];
464 $mockExtension->expects( $this->once() )
465 ->method( 'modifyWatchedItemsWithRCInfo' )
467 $this->identicalTo( $user ),
468 $this->isType( 'array' ),
469 $this->isInstanceOf( IDatabase
::class ),
470 $this->isType( 'array' ),
472 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
474 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
475 foreach ( $items as $i => &$item ) {
476 $item[1]['extension_dummy_field'] = $i;
480 $this->assertNull( $startFrom );
481 $startFrom = [ '20160203123456', 42 ];
484 $queryService = $this->newService( $mockDb );
485 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
488 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
489 $user, [], $startFrom
492 $this->assertInternalType( 'array', $items );
493 $this->assertCount( 2, $items );
495 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
496 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
497 $this->assertInternalType( 'array', $recentChangeInfo );
501 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
508 'rc_title' => 'Foo1',
509 'rc_timestamp' => '20151212010101',
512 'extension_dummy_field' => 0,
518 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
525 'rc_title' => 'Foo2',
526 'rc_timestamp' => '20151212010102',
529 'extension_dummy_field' => 1,
534 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
537 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
540 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
543 [ 'rc_type', 'rc_minor', 'rc_bot' ],
549 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
551 [ 'actormigration' => 'table' ],
552 [ 'rc_user_text' => 'actormigration_user_text' ],
555 [ 'actormigration' => 'join' ],
558 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
560 [ 'actormigration' => 'table' ],
561 [ 'rc_user' => 'actormigration_user' ],
564 [ 'actormigration' => 'join' ],
567 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
569 [ 'commentstore' => 'table' ],
570 [ 'commentstore' => 'field' ],
573 [ 'commentstore' => 'join' ],
576 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
579 [ 'rc_patrolled', 'rc_log_type' ],
585 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
588 [ 'rc_old_len', 'rc_new_len' ],
594 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
597 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
603 [ 'namespaceIds' => [ 0, 1 ] ],
607 [ 'wl_namespace' => [ 0, 1 ] ],
612 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
616 [ 'wl_namespace' => [ 0, 1 ] ],
621 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
625 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
630 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
635 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
639 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
644 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
648 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
652 [ "rc_timestamp <= '20151212010101'" ],
653 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
657 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
661 [ "rc_timestamp >= '20151212010101'" ],
662 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
667 'dir' => WatchedItemQueryService
::DIR_OLDER
,
668 'start' => '20151212020101',
669 'end' => '20151212010101'
674 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
675 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
679 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
683 [ "rc_timestamp >= '20151212010101'" ],
684 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
688 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
692 [ "rc_timestamp <= '20151212010101'" ],
693 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
698 'dir' => WatchedItemQueryService
::DIR_NEWER
,
699 'start' => '20151212010101',
700 'end' => '20151212020101'
705 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
706 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
719 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
728 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
737 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
746 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
755 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
764 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
766 [ 'actormigration' => 'table' ],
768 [ 'actormigration is anon' ],
770 [ 'actormigration' => 'join' ],
773 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
775 [ 'actormigration' => 'table' ],
777 [ 'actormigration is not anon' ],
779 [ 'actormigration' => 'join' ],
782 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
786 [ 'rc_patrolled != 0' ],
791 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
795 [ 'rc_patrolled' => 0 ],
800 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
804 [ 'rc_timestamp >= wl_notificationtimestamp' ],
809 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
813 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
818 [ 'onlyByUser' => 'SomeOtherUser' ],
820 [ 'actormigration' => 'table' ],
822 [ 'actormigration_conds' ],
824 [ 'actormigration' => 'join' ],
827 [ 'notByUser' => 'SomeOtherUser' ],
829 [ 'actormigration' => 'table' ],
831 [ 'NOT(actormigration_conds)' ],
833 [ 'actormigration' => 'join' ],
836 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
837 [ '20151212010101', 123 ],
841 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
843 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
847 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
848 [ '20151212010101', 123 ],
852 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
854 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
858 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
859 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
863 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
865 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
872 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
874 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
877 array $expectedExtraTables,
878 array $expectedExtraFields,
879 array $expectedExtraConds,
880 array $expectedDbOptions,
881 array $expectedExtraJoinConds
883 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
884 $expectedFields = array_merge(
892 'wl_notificationtimestamp',
900 $expectedConds = array_merge(
901 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
904 $expectedJoinConds = array_merge(
909 'wl_namespace=rc_namespace',
918 $expectedExtraJoinConds
921 $mockDb = $this->getMockDb();
922 $mockDb->expects( $this->once() )
928 $this->isType( 'string' ),
932 ->will( $this->returnValue( [] ) );
934 $queryService = $this->newService( $mockDb );
935 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
937 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
939 $this->assertEmpty( $items );
940 $this->assertNull( $startFrom );
943 public function filterPatrolledOptionProvider() {
945 [ WatchedItemQueryService
::FILTER_PATROLLED
],
946 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
951 * @dataProvider filterPatrolledOptionProvider
953 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
956 $mockDb = $this->getMockDb();
957 $mockDb->expects( $this->once() )
960 [ 'recentchanges', 'watchlist', 'page' ],
961 $this->isType( 'array' ),
962 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
963 $this->isType( 'string' ),
964 $this->isType( 'array' ),
965 $this->isType( 'array' )
967 ->will( $this->returnValue( [] ) );
969 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
971 $queryService = $this->newService( $mockDb );
972 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
974 [ 'filters' => [ $filtersOption ] ]
977 $this->assertEmpty( $items );
980 public function mysqlIndexOptimizationProvider() {
985 [ "rc_timestamp > ''" ],
989 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
990 [ "rc_timestamp <= '20151212010101'" ],
994 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
995 [ "rc_timestamp >= '20151212010101'" ],
1006 * @dataProvider mysqlIndexOptimizationProvider
1008 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1011 array $expectedExtraConds
1013 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1014 $conds = array_merge( $commonConds, $expectedExtraConds );
1016 $mockDb = $this->getMockDb();
1017 $mockDb->expects( $this->once() )
1018 ->method( 'select' )
1020 [ 'recentchanges', 'watchlist', 'page' ],
1021 $this->isType( 'array' ),
1023 $this->isType( 'string' ),
1024 $this->isType( 'array' ),
1025 $this->isType( 'array' )
1027 ->will( $this->returnValue( [] ) );
1028 $mockDb->expects( $this->any() )
1029 ->method( 'getType' )
1030 ->will( $this->returnValue( $dbType ) );
1032 $queryService = $this->newService( $mockDb );
1033 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1035 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1037 $this->assertEmpty( $items );
1040 public function userPermissionRelatedExtraChecksProvider() {
1047 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1048 LogPage
::DELETED_ACTION
. ')'
1057 '(rc_type != ' . RC_LOG
. ') OR (' .
1058 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1059 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1068 '(rc_type != ' . RC_LOG
. ') OR (' .
1069 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1070 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1075 [ 'onlyByUser' => 'SomeOtherUser' ],
1077 [ 'actormigration' => 'table' ],
1079 'actormigration_conds',
1080 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1081 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1082 LogPage
::DELETED_ACTION
. ')'
1084 [ 'actormigration' => 'join' ],
1087 [ 'onlyByUser' => 'SomeOtherUser' ],
1089 [ 'actormigration' => 'table' ],
1091 'actormigration_conds',
1092 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1093 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1094 '(rc_type != ' . RC_LOG
. ') OR (' .
1095 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1096 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1098 [ 'actormigration' => 'join' ],
1101 [ 'onlyByUser' => 'SomeOtherUser' ],
1103 [ 'actormigration' => 'table' ],
1105 'actormigration_conds',
1106 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1107 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1108 '(rc_type != ' . RC_LOG
. ') OR (' .
1109 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1110 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1112 [ 'actormigration' => 'join' ],
1118 * @dataProvider userPermissionRelatedExtraChecksProvider
1120 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1123 array $expectedExtraTables,
1124 array $expectedExtraConds,
1125 array $expectedExtraJoins
1127 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1128 $conds = array_merge( $commonConds, $expectedExtraConds );
1130 $mockDb = $this->getMockDb();
1131 $mockDb->expects( $this->once() )
1132 ->method( 'select' )
1134 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1135 $this->isType( 'array' ),
1137 $this->isType( 'string' ),
1138 $this->isType( 'array' ),
1140 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1141 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1142 ], $expectedExtraJoins )
1144 ->will( $this->returnValue( [] ) );
1146 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1148 $queryService = $this->newService( $mockDb );
1149 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1151 $this->assertEmpty( $items );
1154 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1155 $mockDb = $this->getMockDb();
1156 $mockDb->expects( $this->once() )
1157 ->method( 'select' )
1159 [ 'recentchanges', 'watchlist' ],
1167 'wl_notificationtimestamp',
1173 [ 'wl_user' => 1, ],
1174 $this->isType( 'string' ),
1180 'wl_namespace=rc_namespace',
1186 ->will( $this->returnValue( [] ) );
1188 $queryService = $this->newService( $mockDb );
1189 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1191 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1193 $this->assertEmpty( $items );
1196 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1199 [ 'rcTypes' => [ 1337 ] ],
1201 'Bad value for parameter $options[\'rcTypes\']',
1204 [ 'rcTypes' => [ 'edit' ] ],
1206 'Bad value for parameter $options[\'rcTypes\']',
1209 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1211 'Bad value for parameter $options[\'rcTypes\']',
1216 'Bad value for parameter $options[\'dir\']',
1219 [ 'start' => '20151212010101' ],
1221 'Bad value for parameter $options[\'dir\']: must be provided',
1224 [ 'end' => '20151212010101' ],
1226 'Bad value for parameter $options[\'dir\']: must be provided',
1230 [ '20151212010101', 123 ],
1231 'Bad value for parameter $options[\'dir\']: must be provided',
1234 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1236 'Bad value for parameter $startFrom: must be a two-element array',
1239 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1240 [ '20151212010101' ],
1241 'Bad value for parameter $startFrom: must be a two-element array',
1244 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1245 [ '20151212010101', 123, 'foo' ],
1246 'Bad value for parameter $startFrom: must be a two-element array',
1249 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1251 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1254 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1256 'Bad value for parameter $options[\'watchlistOwner\']',
1262 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1264 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1267 $expectedInExceptionMessage
1269 $mockDb = $this->getMockDb();
1270 $mockDb->expects( $this->never() )
1271 ->method( $this->anything() );
1273 $queryService = $this->newService( $mockDb );
1274 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1276 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1277 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1280 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1281 $mockDb = $this->getMockDb();
1282 $mockDb->expects( $this->once() )
1283 ->method( 'select' )
1285 [ 'recentchanges', 'watchlist', 'page' ],
1293 'wl_notificationtimestamp',
1296 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1297 $this->isType( 'string' ),
1303 'wl_namespace=rc_namespace',
1309 'rc_cur_id=page_id',
1313 ->will( $this->returnValue( [] ) );
1315 $queryService = $this->newService( $mockDb );
1316 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1318 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1320 [ 'usedInGenerator' => true ]
1323 $this->assertEmpty( $items );
1326 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1327 $mockDb = $this->getMockDb();
1328 $mockDb->expects( $this->once() )
1329 ->method( 'select' )
1331 [ 'recentchanges', 'watchlist' ],
1339 'wl_notificationtimestamp',
1343 $this->isType( 'string' ),
1349 'wl_namespace=rc_namespace',
1355 ->will( $this->returnValue( [] ) );
1357 $queryService = $this->newService( $mockDb );
1358 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1360 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1362 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1365 $this->assertEmpty( $items );
1368 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1369 $mockDb = $this->getMockDb();
1370 $mockDb->expects( $this->once() )
1371 ->method( 'select' )
1373 $this->isType( 'array' ),
1374 $this->isType( 'array' ),
1377 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1379 $this->isType( 'string' ),
1380 $this->isType( 'array' ),
1381 $this->isType( 'array' )
1383 ->will( $this->returnValue( [] ) );
1385 $queryService = $this->newService( $mockDb );
1386 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1387 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1388 $otherUser->expects( $this->once() )
1389 ->method( 'getOption' )
1390 ->with( 'watchlisttoken' )
1391 ->willReturn( '0123456789abcdef' );
1393 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1395 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1398 $this->assertEmpty( $items );
1401 public function invalidWatchlistTokenProvider() {
1409 * @dataProvider invalidWatchlistTokenProvider
1411 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1412 $mockDb = $this->getMockDb();
1413 $mockDb->expects( $this->never() )
1414 ->method( $this->anything() );
1416 $queryService = $this->newService( $mockDb );
1417 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1418 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1419 $otherUser->expects( $this->once() )
1420 ->method( 'getOption' )
1421 ->with( 'watchlisttoken' )
1422 ->willReturn( '0123456789abcdef' );
1424 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1425 $queryService->getWatchedItemsWithRecentChangeInfo(
1427 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1431 public function testGetWatchedItemsForUser() {
1432 $mockDb = $this->getMockDb();
1433 $mockDb->expects( $this->once() )
1434 ->method( 'select' )
1437 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1440 ->will( $this->returnValue( [
1441 $this->getFakeRow( [
1442 'wl_namespace' => 0,
1443 'wl_title' => 'Foo1',
1444 'wl_notificationtimestamp' => '20151212010101',
1446 $this->getFakeRow( [
1447 'wl_namespace' => 1,
1448 'wl_title' => 'Foo2',
1449 'wl_notificationtimestamp' => null,
1453 $queryService = $this->newService( $mockDb );
1454 $user = $this->getMockNonAnonUserWithId( 1 );
1456 $items = $queryService->getWatchedItemsForUser( $user );
1458 $this->assertInternalType( 'array', $items );
1459 $this->assertCount( 2, $items );
1460 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1461 $this->assertEquals(
1462 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1465 $this->assertEquals(
1466 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1471 public function provideGetWatchedItemsForUserOptions() {
1474 [ 'namespaceIds' => [ 0, 1 ], ],
1475 [ 'wl_namespace' => [ 0, 1 ], ],
1479 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1481 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1485 'namespaceIds' => [ 0 ],
1486 'sort' => WatchedItemQueryService
::SORT_ASC
,
1488 [ 'wl_namespace' => [ 0 ], ],
1489 [ 'ORDER BY' => 'wl_title ASC' ]
1498 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1499 'limit' => "10; DROP TABLE watchlist;\n--",
1501 [ 'wl_namespace' => [ 0, 1 ], ],
1505 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1506 [ 'wl_notificationtimestamp IS NOT NULL' ],
1510 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1511 [ 'wl_notificationtimestamp IS NULL' ],
1515 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1517 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1521 'namespaceIds' => [ 0 ],
1522 'sort' => WatchedItemQueryService
::SORT_DESC
,
1524 [ 'wl_namespace' => [ 0 ], ],
1525 [ 'ORDER BY' => 'wl_title DESC' ]
1531 * @dataProvider provideGetWatchedItemsForUserOptions
1533 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1535 array $expectedConds,
1536 array $expectedDbOptions
1538 $mockDb = $this->getMockDb();
1539 $user = $this->getMockNonAnonUserWithId( 1 );
1541 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1542 $mockDb->expects( $this->once() )
1543 ->method( 'select' )
1546 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1548 $this->isType( 'string' ),
1551 ->will( $this->returnValue( [] ) );
1553 $queryService = $this->newService( $mockDb );
1555 $items = $queryService->getWatchedItemsForUser( $user, $options );
1556 $this->assertEmpty( $items );
1559 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1563 'from' => new TitleValue( 0, 'SomeDbKey' ),
1564 'sort' => WatchedItemQueryService
::SORT_ASC
1566 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1567 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1571 'from' => new TitleValue( 0, 'SomeDbKey' ),
1572 'sort' => WatchedItemQueryService
::SORT_DESC
,
1574 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1575 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1579 'until' => new TitleValue( 0, 'SomeDbKey' ),
1580 'sort' => WatchedItemQueryService
::SORT_ASC
1582 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1583 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1587 'until' => new TitleValue( 0, 'SomeDbKey' ),
1588 'sort' => WatchedItemQueryService
::SORT_DESC
1590 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1591 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1595 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1596 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1597 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1598 'sort' => WatchedItemQueryService
::SORT_ASC
1601 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1602 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1603 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1605 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1609 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1610 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1611 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1612 'sort' => WatchedItemQueryService
::SORT_DESC
1615 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1616 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1617 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1619 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1625 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1627 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1629 array $expectedConds,
1630 array $expectedDbOptions
1632 $user = $this->getMockNonAnonUserWithId( 1 );
1634 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1636 $mockDb = $this->getMockDb();
1637 $mockDb->expects( $this->any() )
1638 ->method( 'addQuotes' )
1639 ->will( $this->returnCallback( function ( $value ) {
1642 $mockDb->expects( $this->any() )
1643 ->method( 'makeList' )
1645 $this->isType( 'array' ),
1646 $this->isType( 'int' )
1648 ->will( $this->returnCallback( function ( $a, $conj ) {
1649 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1650 return implode( $sqlConj, array_map( function ( $s ) {
1651 return '(' . $s . ')';
1655 $mockDb->expects( $this->once() )
1656 ->method( 'select' )
1659 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1661 $this->isType( 'string' ),
1664 ->will( $this->returnValue( [] ) );
1666 $queryService = $this->newService( $mockDb );
1668 $items = $queryService->getWatchedItemsForUser( $user, $options );
1669 $this->assertEmpty( $items );
1672 public function getWatchedItemsForUserInvalidOptionsProvider() {
1675 [ 'sort' => 'foo' ],
1676 'Bad value for parameter $options[\'sort\']'
1679 [ 'filter' => 'foo' ],
1680 'Bad value for parameter $options[\'filter\']'
1683 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1684 'Bad value for parameter $options[\'sort\']: must be provided'
1687 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1688 'Bad value for parameter $options[\'sort\']: must be provided'
1691 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1692 'Bad value for parameter $options[\'sort\']: must be provided'
1698 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1700 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1702 $expectedInExceptionMessage
1704 $queryService = $this->newService( $this->getMockDb() );
1706 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1707 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1710 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1711 $mockDb = $this->getMockDb();
1713 $mockDb->expects( $this->never() )
1714 ->method( $this->anything() );
1716 $queryService = $this->newService( $mockDb );
1718 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1719 $this->assertEmpty( $items );