3 use Wikimedia\Rdbms\IDatabase
;
4 use Wikimedia\Rdbms\LoadBalancer
;
5 use Wikimedia\TestingAccessWrapper
;
8 * @covers WatchedItemQueryService
10 class WatchedItemQueryServiceUnitTest
extends MediaWikiTestCase
{
12 use MediaWikiCoversValidator
;
15 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
17 private function getMockCommentStore() {
18 $mockStore = $this->getMockBuilder( CommentStore
::class )
19 ->disableOriginalConstructor()
21 $mockStore->expects( $this->any() )
22 ->method( 'getFields' )
23 ->willReturn( [ 'commentstore' => 'fields' ] );
24 $mockStore->expects( $this->any() )
27 'tables' => [ 'commentstore' => 'table' ],
28 'fields' => [ 'commentstore' => 'field' ],
29 'joins' => [ 'commentstore' => 'join' ],
35 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
37 private function getMockActorMigration() {
38 $mockStore = $this->getMockBuilder( ActorMigration
::class )
39 ->disableOriginalConstructor()
41 $mockStore->expects( $this->any() )
44 'tables' => [ 'actormigration' => 'table' ],
46 'rc_user' => 'actormigration_user',
47 'rc_user_text' => 'actormigration_user_text',
48 'rc_actor' => 'actormigration_actor',
50 'joins' => [ 'actormigration' => 'join' ],
52 $mockStore->expects( $this->any() )
53 ->method( 'getWhere' )
55 'tables' => [ 'actormigration' => 'table' ],
56 'conds' => 'actormigration_conds',
57 'joins' => [ 'actormigration' => 'join' ],
59 $mockStore->expects( $this->any() )
61 ->willReturn( 'actormigration is anon' );
62 $mockStore->expects( $this->any() )
63 ->method( 'isNotAnon' )
64 ->willReturn( 'actormigration is not anon' );
69 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
70 * @return WatchedItemQueryService
72 private function newService( $mockDb ) {
73 return new WatchedItemQueryService(
74 $this->getMockLoadBalancer( $mockDb ),
75 $this->getMockCommentStore(),
76 $this->getMockActorMigration(),
77 $this->getMockWatchedItemStore()
82 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
84 private function getMockDb() {
85 $mock = $this->createMock( IDatabase
::class );
87 $mock->expects( $this->any() )
88 ->method( 'makeList' )
90 $this->isType( 'array' ),
91 $this->isType( 'int' )
93 ->will( $this->returnCallback( function ( $a, $conj ) {
94 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
96 foreach ( $a as $k => $v ) {
99 } elseif ( is_array( $v ) ) {
100 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
102 $conds[] = "($k = '$v')";
105 return implode( $sqlConj, $conds );
108 $mock->expects( $this->any() )
109 ->method( 'addQuotes' )
110 ->will( $this->returnCallback( function ( $value ) {
114 $mock->expects( $this->any() )
115 ->method( 'timestamp' )
116 ->will( $this->returnArgument( 0 ) );
118 $mock->expects( $this->any() )
120 ->willReturnCallback( function ( $a, $b ) {
128 * @param PHPUnit_Framework_MockObject_MockObject|IDatabase $mockDb
129 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
131 private function getMockLoadBalancer( $mockDb ) {
132 $mock = $this->getMockBuilder( LoadBalancer
::class )
133 ->disableOriginalConstructor()
135 $mock->expects( $this->any() )
136 ->method( 'getConnectionRef' )
138 ->will( $this->returnValue( $mockDb ) );
143 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
145 private function getMockWatchedItemStore() {
146 $mock = $this->getMockBuilder( WatchedItemStore
::class )
147 ->disableOriginalConstructor()
149 $mock->expects( $this->any() )
150 ->method( 'getLatestNotificationTimestamp' )
151 ->will( $this->returnCallback( function ( $timestamp ) {
159 * @return PHPUnit_Framework_MockObject_MockObject|User
161 private function getMockNonAnonUserWithId( $id ) {
162 $mock = $this->getMockBuilder( User
::class )->getMock();
163 $mock->expects( $this->any() )
165 ->will( $this->returnValue( false ) );
166 $mock->expects( $this->any() )
168 ->will( $this->returnValue( $id ) );
174 * @return PHPUnit_Framework_MockObject_MockObject|User
176 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
177 $mock = $this->getMockNonAnonUserWithId( $id );
178 $mock->expects( $this->any() )
179 ->method( 'isAllowed' )
180 ->will( $this->returnValue( true ) );
181 $mock->expects( $this->any() )
182 ->method( 'isAllowedAny' )
183 ->will( $this->returnValue( true ) );
184 $mock->expects( $this->any() )
185 ->method( 'useRCPatrol' )
186 ->will( $this->returnValue( true ) );
192 * @param string $notAllowedAction
193 * @return PHPUnit_Framework_MockObject_MockObject|User
195 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
196 $mock = $this->getMockNonAnonUserWithId( $id );
198 $mock->expects( $this->any() )
199 ->method( 'isAllowed' )
200 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
201 return $action !== $notAllowedAction;
203 $mock->expects( $this->any() )
204 ->method( 'isAllowedAny' )
205 ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
206 return !in_array( $notAllowedAction, $actions );
214 * @return PHPUnit_Framework_MockObject_MockObject|User
216 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
217 $mock = $this->getMockNonAnonUserWithId( $id );
219 $mock->expects( $this->any() )
220 ->method( 'isAllowed' )
221 ->will( $this->returnValue( true ) );
222 $mock->expects( $this->any() )
223 ->method( 'isAllowedAny' )
224 ->will( $this->returnValue( true ) );
226 $mock->expects( $this->any() )
227 ->method( 'useRCPatrol' )
228 ->will( $this->returnValue( false ) );
229 $mock->expects( $this->any() )
230 ->method( 'useNPPatrol' )
231 ->will( $this->returnValue( false ) );
236 private function getMockAnonUser() {
237 $mock = $this->getMockBuilder( User
::class )->getMock();
238 $mock->expects( $this->any() )
240 ->will( $this->returnValue( true ) );
244 private function getFakeRow( array $rowValues ) {
245 $fakeRow = new stdClass();
246 foreach ( $rowValues as $valueName => $value ) {
247 $fakeRow->$valueName = $value;
252 public function testGetWatchedItemsWithRecentChangeInfo() {
253 $mockDb = $this->getMockDb();
254 $mockDb->expects( $this->once() )
257 [ 'recentchanges', 'watchlist', 'page' ],
265 'wl_notificationtimestamp',
272 '(rc_this_oldid=page_latest) OR (rc_type=3)',
274 $this->isType( 'string' ),
282 'wl_namespace=rc_namespace',
292 ->will( $this->returnValue( [
296 'rc_title' => 'Foo1',
297 'rc_timestamp' => '20151212010101',
300 'wl_notificationtimestamp' => '20151212010101',
305 'rc_title' => 'Foo2',
306 'rc_timestamp' => '20151212010102',
309 'wl_notificationtimestamp' => null,
314 'rc_title' => 'Foo3',
315 'rc_timestamp' => '20151212010103',
318 'wl_notificationtimestamp' => null,
322 $queryService = $this->newService( $mockDb );
323 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
326 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
327 $user, [ 'limit' => 2 ], $startFrom
330 $this->assertInternalType( 'array', $items );
331 $this->assertCount( 2, $items );
333 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
334 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
335 $this->assertInternalType( 'array', $recentChangeInfo );
339 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
346 'rc_title' => 'Foo1',
347 'rc_timestamp' => '20151212010101',
355 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
362 'rc_title' => 'Foo2',
363 'rc_timestamp' => '20151212010102',
370 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
373 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
374 $mockDb = $this->getMockDb();
375 $mockDb->expects( $this->once() )
378 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
386 'wl_notificationtimestamp',
390 'extension_dummy_field',
394 '(rc_this_oldid=page_latest) OR (rc_type=3)',
395 'extension_dummy_cond',
397 $this->isType( 'string' ),
399 'extension_dummy_option',
405 'wl_namespace=rc_namespace',
413 'extension_dummy_join_cond' => [],
416 ->will( $this->returnValue( [
420 'rc_title' => 'Foo1',
421 'rc_timestamp' => '20151212010101',
424 'wl_notificationtimestamp' => '20151212010101',
429 'rc_title' => 'Foo2',
430 'rc_timestamp' => '20151212010102',
433 'wl_notificationtimestamp' => null,
437 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
439 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
441 $mockExtension->expects( $this->once() )
442 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
444 $this->identicalTo( $user ),
445 $this->isType( 'array' ),
446 $this->isInstanceOf( IDatabase
::class ),
447 $this->isType( 'array' ),
448 $this->isType( 'array' ),
449 $this->isType( 'array' ),
450 $this->isType( 'array' ),
451 $this->isType( 'array' )
453 ->will( $this->returnCallback( function (
454 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
456 $tables[] = 'extension_dummy_table';
457 $fields[] = 'extension_dummy_field';
458 $conds[] = 'extension_dummy_cond';
459 $dbOptions[] = 'extension_dummy_option';
460 $joinConds['extension_dummy_join_cond'] = [];
462 $mockExtension->expects( $this->once() )
463 ->method( 'modifyWatchedItemsWithRCInfo' )
465 $this->identicalTo( $user ),
466 $this->isType( 'array' ),
467 $this->isInstanceOf( IDatabase
::class ),
468 $this->isType( 'array' ),
470 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
472 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
473 foreach ( $items as $i => &$item ) {
474 $item[1]['extension_dummy_field'] = $i;
478 $this->assertNull( $startFrom );
479 $startFrom = [ '20160203123456', 42 ];
482 $queryService = $this->newService( $mockDb );
483 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
486 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
487 $user, [], $startFrom
490 $this->assertInternalType( 'array', $items );
491 $this->assertCount( 2, $items );
493 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
494 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
495 $this->assertInternalType( 'array', $recentChangeInfo );
499 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
506 'rc_title' => 'Foo1',
507 'rc_timestamp' => '20151212010101',
510 'extension_dummy_field' => 0,
516 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
523 'rc_title' => 'Foo2',
524 'rc_timestamp' => '20151212010102',
527 'extension_dummy_field' => 1,
532 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
535 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
538 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
541 [ 'rc_type', 'rc_minor', 'rc_bot' ],
547 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
549 [ 'actormigration' => 'table' ],
550 [ 'rc_user_text' => 'actormigration_user_text' ],
553 [ 'actormigration' => 'join' ],
556 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
558 [ 'actormigration' => 'table' ],
559 [ 'rc_user' => 'actormigration_user' ],
562 [ 'actormigration' => 'join' ],
565 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
567 [ 'commentstore' => 'table' ],
568 [ 'commentstore' => 'field' ],
571 [ 'commentstore' => 'join' ],
574 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
577 [ 'rc_patrolled', 'rc_log_type' ],
583 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
586 [ 'rc_old_len', 'rc_new_len' ],
592 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
595 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
601 [ 'namespaceIds' => [ 0, 1 ] ],
605 [ 'wl_namespace' => [ 0, 1 ] ],
610 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
614 [ 'wl_namespace' => [ 0, 1 ] ],
619 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
623 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
628 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
633 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
637 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
642 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
646 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
650 [ "rc_timestamp <= '20151212010101'" ],
651 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
655 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
659 [ "rc_timestamp >= '20151212010101'" ],
660 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
665 'dir' => WatchedItemQueryService
::DIR_OLDER
,
666 'start' => '20151212020101',
667 'end' => '20151212010101'
672 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
673 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
677 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
681 [ "rc_timestamp >= '20151212010101'" ],
682 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
686 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
690 [ "rc_timestamp <= '20151212010101'" ],
691 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
696 'dir' => WatchedItemQueryService
::DIR_NEWER
,
697 'start' => '20151212010101',
698 'end' => '20151212020101'
703 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
704 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
717 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
726 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
735 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
744 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
753 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
762 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
764 [ 'actormigration' => 'table' ],
766 [ 'actormigration is anon' ],
768 [ 'actormigration' => 'join' ],
771 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
773 [ 'actormigration' => 'table' ],
775 [ 'actormigration is not anon' ],
777 [ 'actormigration' => 'join' ],
780 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
784 [ 'rc_patrolled != 0' ],
789 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
793 [ 'rc_patrolled' => 0 ],
798 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
802 [ 'rc_timestamp >= wl_notificationtimestamp' ],
807 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
811 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
816 [ 'onlyByUser' => 'SomeOtherUser' ],
818 [ 'actormigration' => 'table' ],
820 [ 'actormigration_conds' ],
822 [ 'actormigration' => 'join' ],
825 [ 'notByUser' => 'SomeOtherUser' ],
827 [ 'actormigration' => 'table' ],
829 [ 'NOT(actormigration_conds)' ],
831 [ 'actormigration' => 'join' ],
834 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
835 [ '20151212010101', 123 ],
839 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
841 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
845 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
846 [ '20151212010101', 123 ],
850 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
852 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
856 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
857 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
861 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
863 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
870 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
872 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
875 array $expectedExtraTables,
876 array $expectedExtraFields,
877 array $expectedExtraConds,
878 array $expectedDbOptions,
879 array $expectedExtraJoinConds
881 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
882 $expectedFields = array_merge(
890 'wl_notificationtimestamp',
898 $expectedConds = array_merge(
899 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
902 $expectedJoinConds = array_merge(
907 'wl_namespace=rc_namespace',
916 $expectedExtraJoinConds
919 $mockDb = $this->getMockDb();
920 $mockDb->expects( $this->once() )
926 $this->isType( 'string' ),
930 ->will( $this->returnValue( [] ) );
932 $queryService = $this->newService( $mockDb );
933 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
935 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
937 $this->assertEmpty( $items );
938 $this->assertNull( $startFrom );
941 public function filterPatrolledOptionProvider() {
943 [ WatchedItemQueryService
::FILTER_PATROLLED
],
944 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
949 * @dataProvider filterPatrolledOptionProvider
951 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
954 $mockDb = $this->getMockDb();
955 $mockDb->expects( $this->once() )
958 [ 'recentchanges', 'watchlist', 'page' ],
959 $this->isType( 'array' ),
960 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
961 $this->isType( 'string' ),
962 $this->isType( 'array' ),
963 $this->isType( 'array' )
965 ->will( $this->returnValue( [] ) );
967 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
969 $queryService = $this->newService( $mockDb );
970 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
972 [ 'filters' => [ $filtersOption ] ]
975 $this->assertEmpty( $items );
978 public function mysqlIndexOptimizationProvider() {
983 [ "rc_timestamp > ''" ],
987 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
988 [ "rc_timestamp <= '20151212010101'" ],
992 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
993 [ "rc_timestamp >= '20151212010101'" ],
1004 * @dataProvider mysqlIndexOptimizationProvider
1006 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1009 array $expectedExtraConds
1011 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1012 $conds = array_merge( $commonConds, $expectedExtraConds );
1014 $mockDb = $this->getMockDb();
1015 $mockDb->expects( $this->once() )
1016 ->method( 'select' )
1018 [ 'recentchanges', 'watchlist', 'page' ],
1019 $this->isType( 'array' ),
1021 $this->isType( 'string' ),
1022 $this->isType( 'array' ),
1023 $this->isType( 'array' )
1025 ->will( $this->returnValue( [] ) );
1026 $mockDb->expects( $this->any() )
1027 ->method( 'getType' )
1028 ->will( $this->returnValue( $dbType ) );
1030 $queryService = $this->newService( $mockDb );
1031 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1033 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1035 $this->assertEmpty( $items );
1038 public function userPermissionRelatedExtraChecksProvider() {
1045 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1046 LogPage
::DELETED_ACTION
. ')'
1055 '(rc_type != ' . RC_LOG
. ') OR (' .
1056 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1057 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1066 '(rc_type != ' . RC_LOG
. ') OR (' .
1067 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1068 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1073 [ 'onlyByUser' => 'SomeOtherUser' ],
1075 [ 'actormigration' => 'table' ],
1077 'actormigration_conds',
1078 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1079 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1080 LogPage
::DELETED_ACTION
. ')'
1082 [ 'actormigration' => 'join' ],
1085 [ 'onlyByUser' => 'SomeOtherUser' ],
1087 [ 'actormigration' => 'table' ],
1089 'actormigration_conds',
1090 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1091 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1092 '(rc_type != ' . RC_LOG
. ') OR (' .
1093 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1094 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1096 [ 'actormigration' => 'join' ],
1099 [ 'onlyByUser' => 'SomeOtherUser' ],
1101 [ 'actormigration' => 'table' ],
1103 'actormigration_conds',
1104 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1105 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1106 '(rc_type != ' . RC_LOG
. ') OR (' .
1107 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1108 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1110 [ 'actormigration' => 'join' ],
1116 * @dataProvider userPermissionRelatedExtraChecksProvider
1118 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1121 array $expectedExtraTables,
1122 array $expectedExtraConds,
1123 array $expectedExtraJoins
1125 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1126 $conds = array_merge( $commonConds, $expectedExtraConds );
1128 $mockDb = $this->getMockDb();
1129 $mockDb->expects( $this->once() )
1130 ->method( 'select' )
1132 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1133 $this->isType( 'array' ),
1135 $this->isType( 'string' ),
1136 $this->isType( 'array' ),
1138 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1139 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1140 ], $expectedExtraJoins )
1142 ->will( $this->returnValue( [] ) );
1144 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1146 $queryService = $this->newService( $mockDb );
1147 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1149 $this->assertEmpty( $items );
1152 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1153 $mockDb = $this->getMockDb();
1154 $mockDb->expects( $this->once() )
1155 ->method( 'select' )
1157 [ 'recentchanges', 'watchlist' ],
1165 'wl_notificationtimestamp',
1171 [ 'wl_user' => 1, ],
1172 $this->isType( 'string' ),
1178 'wl_namespace=rc_namespace',
1184 ->will( $this->returnValue( [] ) );
1186 $queryService = $this->newService( $mockDb );
1187 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1189 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1191 $this->assertEmpty( $items );
1194 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1197 [ 'rcTypes' => [ 1337 ] ],
1199 'Bad value for parameter $options[\'rcTypes\']',
1202 [ 'rcTypes' => [ 'edit' ] ],
1204 'Bad value for parameter $options[\'rcTypes\']',
1207 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1209 'Bad value for parameter $options[\'rcTypes\']',
1214 'Bad value for parameter $options[\'dir\']',
1217 [ 'start' => '20151212010101' ],
1219 'Bad value for parameter $options[\'dir\']: must be provided',
1222 [ 'end' => '20151212010101' ],
1224 'Bad value for parameter $options[\'dir\']: must be provided',
1228 [ '20151212010101', 123 ],
1229 'Bad value for parameter $options[\'dir\']: must be provided',
1232 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1234 'Bad value for parameter $startFrom: must be a two-element array',
1237 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1238 [ '20151212010101' ],
1239 'Bad value for parameter $startFrom: must be a two-element array',
1242 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1243 [ '20151212010101', 123, 'foo' ],
1244 'Bad value for parameter $startFrom: must be a two-element array',
1247 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1249 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1252 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1254 'Bad value for parameter $options[\'watchlistOwner\']',
1260 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1262 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1265 $expectedInExceptionMessage
1267 $mockDb = $this->getMockDb();
1268 $mockDb->expects( $this->never() )
1269 ->method( $this->anything() );
1271 $queryService = $this->newService( $mockDb );
1272 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1274 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1275 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1278 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1279 $mockDb = $this->getMockDb();
1280 $mockDb->expects( $this->once() )
1281 ->method( 'select' )
1283 [ 'recentchanges', 'watchlist', 'page' ],
1291 'wl_notificationtimestamp',
1294 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1295 $this->isType( 'string' ),
1301 'wl_namespace=rc_namespace',
1307 'rc_cur_id=page_id',
1311 ->will( $this->returnValue( [] ) );
1313 $queryService = $this->newService( $mockDb );
1314 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1316 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1318 [ 'usedInGenerator' => true ]
1321 $this->assertEmpty( $items );
1324 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1325 $mockDb = $this->getMockDb();
1326 $mockDb->expects( $this->once() )
1327 ->method( 'select' )
1329 [ 'recentchanges', 'watchlist' ],
1337 'wl_notificationtimestamp',
1341 $this->isType( 'string' ),
1347 'wl_namespace=rc_namespace',
1353 ->will( $this->returnValue( [] ) );
1355 $queryService = $this->newService( $mockDb );
1356 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1358 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1360 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1363 $this->assertEmpty( $items );
1366 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1367 $mockDb = $this->getMockDb();
1368 $mockDb->expects( $this->once() )
1369 ->method( 'select' )
1371 $this->isType( 'array' ),
1372 $this->isType( 'array' ),
1375 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1377 $this->isType( 'string' ),
1378 $this->isType( 'array' ),
1379 $this->isType( 'array' )
1381 ->will( $this->returnValue( [] ) );
1383 $queryService = $this->newService( $mockDb );
1384 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1385 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1386 $otherUser->expects( $this->once() )
1387 ->method( 'getOption' )
1388 ->with( 'watchlisttoken' )
1389 ->willReturn( '0123456789abcdef' );
1391 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1393 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1396 $this->assertEmpty( $items );
1399 public function invalidWatchlistTokenProvider() {
1407 * @dataProvider invalidWatchlistTokenProvider
1409 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1410 $mockDb = $this->getMockDb();
1411 $mockDb->expects( $this->never() )
1412 ->method( $this->anything() );
1414 $queryService = $this->newService( $mockDb );
1415 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1416 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1417 $otherUser->expects( $this->once() )
1418 ->method( 'getOption' )
1419 ->with( 'watchlisttoken' )
1420 ->willReturn( '0123456789abcdef' );
1422 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1423 $queryService->getWatchedItemsWithRecentChangeInfo(
1425 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1429 public function testGetWatchedItemsForUser() {
1430 $mockDb = $this->getMockDb();
1431 $mockDb->expects( $this->once() )
1432 ->method( 'select' )
1435 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1438 ->will( $this->returnValue( [
1439 $this->getFakeRow( [
1440 'wl_namespace' => 0,
1441 'wl_title' => 'Foo1',
1442 'wl_notificationtimestamp' => '20151212010101',
1444 $this->getFakeRow( [
1445 'wl_namespace' => 1,
1446 'wl_title' => 'Foo2',
1447 'wl_notificationtimestamp' => null,
1451 $queryService = $this->newService( $mockDb );
1452 $user = $this->getMockNonAnonUserWithId( 1 );
1454 $items = $queryService->getWatchedItemsForUser( $user );
1456 $this->assertInternalType( 'array', $items );
1457 $this->assertCount( 2, $items );
1458 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1459 $this->assertEquals(
1460 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1463 $this->assertEquals(
1464 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1469 public function provideGetWatchedItemsForUserOptions() {
1472 [ 'namespaceIds' => [ 0, 1 ], ],
1473 [ 'wl_namespace' => [ 0, 1 ], ],
1477 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1479 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1483 'namespaceIds' => [ 0 ],
1484 'sort' => WatchedItemQueryService
::SORT_ASC
,
1486 [ 'wl_namespace' => [ 0 ], ],
1487 [ 'ORDER BY' => 'wl_title ASC' ]
1496 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1497 'limit' => "10; DROP TABLE watchlist;\n--",
1499 [ 'wl_namespace' => [ 0, 1 ], ],
1503 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1504 [ 'wl_notificationtimestamp IS NOT NULL' ],
1508 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1509 [ 'wl_notificationtimestamp IS NULL' ],
1513 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1515 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1519 'namespaceIds' => [ 0 ],
1520 'sort' => WatchedItemQueryService
::SORT_DESC
,
1522 [ 'wl_namespace' => [ 0 ], ],
1523 [ 'ORDER BY' => 'wl_title DESC' ]
1529 * @dataProvider provideGetWatchedItemsForUserOptions
1531 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1533 array $expectedConds,
1534 array $expectedDbOptions
1536 $mockDb = $this->getMockDb();
1537 $user = $this->getMockNonAnonUserWithId( 1 );
1539 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1540 $mockDb->expects( $this->once() )
1541 ->method( 'select' )
1544 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1546 $this->isType( 'string' ),
1549 ->will( $this->returnValue( [] ) );
1551 $queryService = $this->newService( $mockDb );
1553 $items = $queryService->getWatchedItemsForUser( $user, $options );
1554 $this->assertEmpty( $items );
1557 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1561 'from' => new TitleValue( 0, 'SomeDbKey' ),
1562 'sort' => WatchedItemQueryService
::SORT_ASC
1564 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1565 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1569 'from' => new TitleValue( 0, 'SomeDbKey' ),
1570 'sort' => WatchedItemQueryService
::SORT_DESC
,
1572 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1573 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1577 'until' => new TitleValue( 0, 'SomeDbKey' ),
1578 'sort' => WatchedItemQueryService
::SORT_ASC
1580 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1581 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1585 'until' => new TitleValue( 0, 'SomeDbKey' ),
1586 'sort' => WatchedItemQueryService
::SORT_DESC
1588 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1589 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1593 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1594 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1595 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1596 'sort' => WatchedItemQueryService
::SORT_ASC
1599 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1600 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1601 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1603 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1607 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1608 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1609 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1610 'sort' => WatchedItemQueryService
::SORT_DESC
1613 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1614 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1615 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1617 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1623 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1625 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1627 array $expectedConds,
1628 array $expectedDbOptions
1630 $user = $this->getMockNonAnonUserWithId( 1 );
1632 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1634 $mockDb = $this->getMockDb();
1635 $mockDb->expects( $this->any() )
1636 ->method( 'addQuotes' )
1637 ->will( $this->returnCallback( function ( $value ) {
1640 $mockDb->expects( $this->any() )
1641 ->method( 'makeList' )
1643 $this->isType( 'array' ),
1644 $this->isType( 'int' )
1646 ->will( $this->returnCallback( function ( $a, $conj ) {
1647 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1648 return implode( $sqlConj, array_map( function ( $s ) {
1649 return '(' . $s . ')';
1653 $mockDb->expects( $this->once() )
1654 ->method( 'select' )
1657 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1659 $this->isType( 'string' ),
1662 ->will( $this->returnValue( [] ) );
1664 $queryService = $this->newService( $mockDb );
1666 $items = $queryService->getWatchedItemsForUser( $user, $options );
1667 $this->assertEmpty( $items );
1670 public function getWatchedItemsForUserInvalidOptionsProvider() {
1673 [ 'sort' => 'foo' ],
1674 'Bad value for parameter $options[\'sort\']'
1677 [ 'filter' => 'foo' ],
1678 'Bad value for parameter $options[\'filter\']'
1681 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1682 'Bad value for parameter $options[\'sort\']: must be provided'
1685 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1686 'Bad value for parameter $options[\'sort\']: must be provided'
1689 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1690 'Bad value for parameter $options[\'sort\']: must be provided'
1696 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1698 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1700 $expectedInExceptionMessage
1702 $queryService = $this->newService( $this->getMockDb() );
1704 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1705 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1708 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1709 $mockDb = $this->getMockDb();
1711 $mockDb->expects( $this->never() )
1712 ->method( $this->anything() );
1714 $queryService = $this->newService( $mockDb );
1716 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1717 $this->assertEmpty( $items );