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 () use ( $notAllowedAction ) {
208 $actions = func_get_args();
209 return !in_array( $notAllowedAction, $actions );
217 * @return PHPUnit_Framework_MockObject_MockObject|User
219 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
220 $mock = $this->getMockNonAnonUserWithId( $id );
222 $mock->expects( $this->any() )
223 ->method( 'isAllowed' )
224 ->will( $this->returnValue( true ) );
225 $mock->expects( $this->any() )
226 ->method( 'isAllowedAny' )
227 ->will( $this->returnValue( true ) );
229 $mock->expects( $this->any() )
230 ->method( 'useRCPatrol' )
231 ->will( $this->returnValue( false ) );
232 $mock->expects( $this->any() )
233 ->method( 'useNPPatrol' )
234 ->will( $this->returnValue( false ) );
239 private function getMockAnonUser() {
240 $mock = $this->getMockBuilder( User
::class )->getMock();
241 $mock->expects( $this->any() )
243 ->will( $this->returnValue( true ) );
247 private function getFakeRow( array $rowValues ) {
248 $fakeRow = new stdClass();
249 foreach ( $rowValues as $valueName => $value ) {
250 $fakeRow->$valueName = $value;
255 public function testGetWatchedItemsWithRecentChangeInfo() {
256 $mockDb = $this->getMockDb();
257 $mockDb->expects( $this->once() )
260 [ 'recentchanges', 'watchlist', 'page' ],
268 'wl_notificationtimestamp',
275 '(rc_this_oldid=page_latest) OR (rc_type=3)',
277 $this->isType( 'string' ),
285 'wl_namespace=rc_namespace',
295 ->will( $this->returnValue( [
299 'rc_title' => 'Foo1',
300 'rc_timestamp' => '20151212010101',
303 'wl_notificationtimestamp' => '20151212010101',
308 'rc_title' => 'Foo2',
309 'rc_timestamp' => '20151212010102',
312 'wl_notificationtimestamp' => null,
317 'rc_title' => 'Foo3',
318 'rc_timestamp' => '20151212010103',
321 'wl_notificationtimestamp' => null,
325 $queryService = $this->newService( $mockDb );
326 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
329 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
330 $user, [ 'limit' => 2 ], $startFrom
333 $this->assertInternalType( 'array', $items );
334 $this->assertCount( 2, $items );
336 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
337 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
338 $this->assertInternalType( 'array', $recentChangeInfo );
342 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
349 'rc_title' => 'Foo1',
350 'rc_timestamp' => '20151212010101',
358 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
365 'rc_title' => 'Foo2',
366 'rc_timestamp' => '20151212010102',
373 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
376 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
377 $mockDb = $this->getMockDb();
378 $mockDb->expects( $this->once() )
381 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
389 'wl_notificationtimestamp',
393 'extension_dummy_field',
397 '(rc_this_oldid=page_latest) OR (rc_type=3)',
398 'extension_dummy_cond',
400 $this->isType( 'string' ),
402 'extension_dummy_option',
408 'wl_namespace=rc_namespace',
416 'extension_dummy_join_cond' => [],
419 ->will( $this->returnValue( [
423 'rc_title' => 'Foo1',
424 'rc_timestamp' => '20151212010101',
427 'wl_notificationtimestamp' => '20151212010101',
432 'rc_title' => 'Foo2',
433 'rc_timestamp' => '20151212010102',
436 'wl_notificationtimestamp' => null,
440 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
442 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
444 $mockExtension->expects( $this->once() )
445 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
447 $this->identicalTo( $user ),
448 $this->isType( 'array' ),
449 $this->isInstanceOf( IDatabase
::class ),
450 $this->isType( 'array' ),
451 $this->isType( 'array' ),
452 $this->isType( 'array' ),
453 $this->isType( 'array' ),
454 $this->isType( 'array' )
456 ->will( $this->returnCallback( function (
457 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
459 $tables[] = 'extension_dummy_table';
460 $fields[] = 'extension_dummy_field';
461 $conds[] = 'extension_dummy_cond';
462 $dbOptions[] = 'extension_dummy_option';
463 $joinConds['extension_dummy_join_cond'] = [];
465 $mockExtension->expects( $this->once() )
466 ->method( 'modifyWatchedItemsWithRCInfo' )
468 $this->identicalTo( $user ),
469 $this->isType( 'array' ),
470 $this->isInstanceOf( IDatabase
::class ),
471 $this->isType( 'array' ),
473 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
475 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
476 foreach ( $items as $i => &$item ) {
477 $item[1]['extension_dummy_field'] = $i;
481 $this->assertNull( $startFrom );
482 $startFrom = [ '20160203123456', 42 ];
485 $queryService = $this->newService( $mockDb );
486 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
489 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
490 $user, [], $startFrom
493 $this->assertInternalType( 'array', $items );
494 $this->assertCount( 2, $items );
496 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
497 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
498 $this->assertInternalType( 'array', $recentChangeInfo );
502 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
509 'rc_title' => 'Foo1',
510 'rc_timestamp' => '20151212010101',
513 'extension_dummy_field' => 0,
519 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
526 'rc_title' => 'Foo2',
527 'rc_timestamp' => '20151212010102',
530 'extension_dummy_field' => 1,
535 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
538 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
541 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
544 [ 'rc_type', 'rc_minor', 'rc_bot' ],
550 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
552 [ 'actormigration' => 'table' ],
553 [ 'rc_user_text' => 'actormigration_user_text' ],
556 [ 'actormigration' => 'join' ],
559 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
561 [ 'actormigration' => 'table' ],
562 [ 'rc_user' => 'actormigration_user' ],
565 [ 'actormigration' => 'join' ],
568 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
570 [ 'commentstore' => 'table' ],
571 [ 'commentstore' => 'field' ],
574 [ 'commentstore' => 'join' ],
577 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
580 [ 'rc_patrolled', 'rc_log_type' ],
586 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
589 [ 'rc_old_len', 'rc_new_len' ],
595 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
598 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
604 [ 'namespaceIds' => [ 0, 1 ] ],
608 [ 'wl_namespace' => [ 0, 1 ] ],
613 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
617 [ 'wl_namespace' => [ 0, 1 ] ],
622 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
626 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
631 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
636 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
640 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
645 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
649 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
653 [ "rc_timestamp <= '20151212010101'" ],
654 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
658 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
662 [ "rc_timestamp >= '20151212010101'" ],
663 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
668 'dir' => WatchedItemQueryService
::DIR_OLDER
,
669 'start' => '20151212020101',
670 'end' => '20151212010101'
675 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
676 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
680 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
684 [ "rc_timestamp >= '20151212010101'" ],
685 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
689 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
693 [ "rc_timestamp <= '20151212010101'" ],
694 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
699 'dir' => WatchedItemQueryService
::DIR_NEWER
,
700 'start' => '20151212010101',
701 'end' => '20151212020101'
706 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
707 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
720 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
729 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
738 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
747 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
756 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
765 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
767 [ 'actormigration' => 'table' ],
769 [ 'actormigration is anon' ],
771 [ 'actormigration' => 'join' ],
774 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
776 [ 'actormigration' => 'table' ],
778 [ 'actormigration is not anon' ],
780 [ 'actormigration' => 'join' ],
783 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
787 [ 'rc_patrolled != 0' ],
792 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
796 [ 'rc_patrolled' => 0 ],
801 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
805 [ 'rc_timestamp >= wl_notificationtimestamp' ],
810 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
814 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
819 [ 'onlyByUser' => 'SomeOtherUser' ],
821 [ 'actormigration' => 'table' ],
823 [ 'actormigration_conds' ],
825 [ 'actormigration' => 'join' ],
828 [ 'notByUser' => 'SomeOtherUser' ],
830 [ 'actormigration' => 'table' ],
832 [ 'NOT(actormigration_conds)' ],
834 [ 'actormigration' => 'join' ],
837 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
838 [ '20151212010101', 123 ],
842 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
844 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
848 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
849 [ '20151212010101', 123 ],
853 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
855 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
859 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
860 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
864 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
866 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
873 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
875 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
878 array $expectedExtraTables,
879 array $expectedExtraFields,
880 array $expectedExtraConds,
881 array $expectedDbOptions,
882 array $expectedExtraJoinConds
884 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
885 $expectedFields = array_merge(
893 'wl_notificationtimestamp',
901 $expectedConds = array_merge(
902 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
905 $expectedJoinConds = array_merge(
910 'wl_namespace=rc_namespace',
919 $expectedExtraJoinConds
922 $mockDb = $this->getMockDb();
923 $mockDb->expects( $this->once() )
929 $this->isType( 'string' ),
933 ->will( $this->returnValue( [] ) );
935 $queryService = $this->newService( $mockDb );
936 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
938 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
940 $this->assertEmpty( $items );
941 $this->assertNull( $startFrom );
944 public function filterPatrolledOptionProvider() {
946 [ WatchedItemQueryService
::FILTER_PATROLLED
],
947 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
952 * @dataProvider filterPatrolledOptionProvider
954 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
957 $mockDb = $this->getMockDb();
958 $mockDb->expects( $this->once() )
961 [ 'recentchanges', 'watchlist', 'page' ],
962 $this->isType( 'array' ),
963 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
964 $this->isType( 'string' ),
965 $this->isType( 'array' ),
966 $this->isType( 'array' )
968 ->will( $this->returnValue( [] ) );
970 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
972 $queryService = $this->newService( $mockDb );
973 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
975 [ 'filters' => [ $filtersOption ] ]
978 $this->assertEmpty( $items );
981 public function mysqlIndexOptimizationProvider() {
986 [ "rc_timestamp > ''" ],
990 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
991 [ "rc_timestamp <= '20151212010101'" ],
995 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
996 [ "rc_timestamp >= '20151212010101'" ],
1007 * @dataProvider mysqlIndexOptimizationProvider
1009 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1012 array $expectedExtraConds
1014 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1015 $conds = array_merge( $commonConds, $expectedExtraConds );
1017 $mockDb = $this->getMockDb();
1018 $mockDb->expects( $this->once() )
1019 ->method( 'select' )
1021 [ 'recentchanges', 'watchlist', 'page' ],
1022 $this->isType( 'array' ),
1024 $this->isType( 'string' ),
1025 $this->isType( 'array' ),
1026 $this->isType( 'array' )
1028 ->will( $this->returnValue( [] ) );
1029 $mockDb->expects( $this->any() )
1030 ->method( 'getType' )
1031 ->will( $this->returnValue( $dbType ) );
1033 $queryService = $this->newService( $mockDb );
1034 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1036 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1038 $this->assertEmpty( $items );
1041 public function userPermissionRelatedExtraChecksProvider() {
1048 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1049 LogPage
::DELETED_ACTION
. ')'
1058 '(rc_type != ' . RC_LOG
. ') OR (' .
1059 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1060 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1069 '(rc_type != ' . RC_LOG
. ') OR (' .
1070 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1071 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1076 [ 'onlyByUser' => 'SomeOtherUser' ],
1078 [ 'actormigration' => 'table' ],
1080 'actormigration_conds',
1081 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1082 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1083 LogPage
::DELETED_ACTION
. ')'
1085 [ 'actormigration' => 'join' ],
1088 [ 'onlyByUser' => 'SomeOtherUser' ],
1090 [ 'actormigration' => 'table' ],
1092 'actormigration_conds',
1093 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1094 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1095 '(rc_type != ' . RC_LOG
. ') OR (' .
1096 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1097 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1099 [ 'actormigration' => 'join' ],
1102 [ 'onlyByUser' => 'SomeOtherUser' ],
1104 [ 'actormigration' => 'table' ],
1106 'actormigration_conds',
1107 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1108 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1109 '(rc_type != ' . RC_LOG
. ') OR (' .
1110 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1111 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1113 [ 'actormigration' => 'join' ],
1119 * @dataProvider userPermissionRelatedExtraChecksProvider
1121 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1124 array $expectedExtraTables,
1125 array $expectedExtraConds,
1126 array $expectedExtraJoins
1128 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1129 $conds = array_merge( $commonConds, $expectedExtraConds );
1131 $mockDb = $this->getMockDb();
1132 $mockDb->expects( $this->once() )
1133 ->method( 'select' )
1135 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1136 $this->isType( 'array' ),
1138 $this->isType( 'string' ),
1139 $this->isType( 'array' ),
1141 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1142 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1143 ], $expectedExtraJoins )
1145 ->will( $this->returnValue( [] ) );
1147 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1149 $queryService = $this->newService( $mockDb );
1150 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1152 $this->assertEmpty( $items );
1155 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1156 $mockDb = $this->getMockDb();
1157 $mockDb->expects( $this->once() )
1158 ->method( 'select' )
1160 [ 'recentchanges', 'watchlist' ],
1168 'wl_notificationtimestamp',
1174 [ 'wl_user' => 1, ],
1175 $this->isType( 'string' ),
1181 'wl_namespace=rc_namespace',
1187 ->will( $this->returnValue( [] ) );
1189 $queryService = $this->newService( $mockDb );
1190 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1192 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1194 $this->assertEmpty( $items );
1197 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1200 [ 'rcTypes' => [ 1337 ] ],
1202 'Bad value for parameter $options[\'rcTypes\']',
1205 [ 'rcTypes' => [ 'edit' ] ],
1207 'Bad value for parameter $options[\'rcTypes\']',
1210 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1212 'Bad value for parameter $options[\'rcTypes\']',
1217 'Bad value for parameter $options[\'dir\']',
1220 [ 'start' => '20151212010101' ],
1222 'Bad value for parameter $options[\'dir\']: must be provided',
1225 [ 'end' => '20151212010101' ],
1227 'Bad value for parameter $options[\'dir\']: must be provided',
1231 [ '20151212010101', 123 ],
1232 'Bad value for parameter $options[\'dir\']: must be provided',
1235 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1237 'Bad value for parameter $startFrom: must be a two-element array',
1240 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1241 [ '20151212010101' ],
1242 'Bad value for parameter $startFrom: must be a two-element array',
1245 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1246 [ '20151212010101', 123, 'foo' ],
1247 'Bad value for parameter $startFrom: must be a two-element array',
1250 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1252 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1255 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1257 'Bad value for parameter $options[\'watchlistOwner\']',
1263 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1265 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1268 $expectedInExceptionMessage
1270 $mockDb = $this->getMockDb();
1271 $mockDb->expects( $this->never() )
1272 ->method( $this->anything() );
1274 $queryService = $this->newService( $mockDb );
1275 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1277 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1278 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1281 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1282 $mockDb = $this->getMockDb();
1283 $mockDb->expects( $this->once() )
1284 ->method( 'select' )
1286 [ 'recentchanges', 'watchlist', 'page' ],
1294 'wl_notificationtimestamp',
1297 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1298 $this->isType( 'string' ),
1304 'wl_namespace=rc_namespace',
1310 'rc_cur_id=page_id',
1314 ->will( $this->returnValue( [] ) );
1316 $queryService = $this->newService( $mockDb );
1317 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1319 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1321 [ 'usedInGenerator' => true ]
1324 $this->assertEmpty( $items );
1327 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1328 $mockDb = $this->getMockDb();
1329 $mockDb->expects( $this->once() )
1330 ->method( 'select' )
1332 [ 'recentchanges', 'watchlist' ],
1340 'wl_notificationtimestamp',
1344 $this->isType( 'string' ),
1350 'wl_namespace=rc_namespace',
1356 ->will( $this->returnValue( [] ) );
1358 $queryService = $this->newService( $mockDb );
1359 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1361 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1363 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1366 $this->assertEmpty( $items );
1369 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1370 $mockDb = $this->getMockDb();
1371 $mockDb->expects( $this->once() )
1372 ->method( 'select' )
1374 $this->isType( 'array' ),
1375 $this->isType( 'array' ),
1378 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1380 $this->isType( 'string' ),
1381 $this->isType( 'array' ),
1382 $this->isType( 'array' )
1384 ->will( $this->returnValue( [] ) );
1386 $queryService = $this->newService( $mockDb );
1387 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1388 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1389 $otherUser->expects( $this->once() )
1390 ->method( 'getOption' )
1391 ->with( 'watchlisttoken' )
1392 ->willReturn( '0123456789abcdef' );
1394 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1396 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1399 $this->assertEmpty( $items );
1402 public function invalidWatchlistTokenProvider() {
1410 * @dataProvider invalidWatchlistTokenProvider
1412 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1413 $mockDb = $this->getMockDb();
1414 $mockDb->expects( $this->never() )
1415 ->method( $this->anything() );
1417 $queryService = $this->newService( $mockDb );
1418 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1419 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1420 $otherUser->expects( $this->once() )
1421 ->method( 'getOption' )
1422 ->with( 'watchlisttoken' )
1423 ->willReturn( '0123456789abcdef' );
1425 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1426 $queryService->getWatchedItemsWithRecentChangeInfo(
1428 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1432 public function testGetWatchedItemsForUser() {
1433 $mockDb = $this->getMockDb();
1434 $mockDb->expects( $this->once() )
1435 ->method( 'select' )
1438 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1441 ->will( $this->returnValue( [
1442 $this->getFakeRow( [
1443 'wl_namespace' => 0,
1444 'wl_title' => 'Foo1',
1445 'wl_notificationtimestamp' => '20151212010101',
1447 $this->getFakeRow( [
1448 'wl_namespace' => 1,
1449 'wl_title' => 'Foo2',
1450 'wl_notificationtimestamp' => null,
1454 $queryService = $this->newService( $mockDb );
1455 $user = $this->getMockNonAnonUserWithId( 1 );
1457 $items = $queryService->getWatchedItemsForUser( $user );
1459 $this->assertInternalType( 'array', $items );
1460 $this->assertCount( 2, $items );
1461 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1462 $this->assertEquals(
1463 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1466 $this->assertEquals(
1467 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1472 public function provideGetWatchedItemsForUserOptions() {
1475 [ 'namespaceIds' => [ 0, 1 ], ],
1476 [ 'wl_namespace' => [ 0, 1 ], ],
1480 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1482 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1486 'namespaceIds' => [ 0 ],
1487 'sort' => WatchedItemQueryService
::SORT_ASC
,
1489 [ 'wl_namespace' => [ 0 ], ],
1490 [ 'ORDER BY' => 'wl_title ASC' ]
1499 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1500 'limit' => "10; DROP TABLE watchlist;\n--",
1502 [ 'wl_namespace' => [ 0, 1 ], ],
1506 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1507 [ 'wl_notificationtimestamp IS NOT NULL' ],
1511 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1512 [ 'wl_notificationtimestamp IS NULL' ],
1516 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1518 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1522 'namespaceIds' => [ 0 ],
1523 'sort' => WatchedItemQueryService
::SORT_DESC
,
1525 [ 'wl_namespace' => [ 0 ], ],
1526 [ 'ORDER BY' => 'wl_title DESC' ]
1532 * @dataProvider provideGetWatchedItemsForUserOptions
1534 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1536 array $expectedConds,
1537 array $expectedDbOptions
1539 $mockDb = $this->getMockDb();
1540 $user = $this->getMockNonAnonUserWithId( 1 );
1542 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1543 $mockDb->expects( $this->once() )
1544 ->method( 'select' )
1547 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1549 $this->isType( 'string' ),
1552 ->will( $this->returnValue( [] ) );
1554 $queryService = $this->newService( $mockDb );
1556 $items = $queryService->getWatchedItemsForUser( $user, $options );
1557 $this->assertEmpty( $items );
1560 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1564 'from' => new TitleValue( 0, 'SomeDbKey' ),
1565 'sort' => WatchedItemQueryService
::SORT_ASC
1567 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1568 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1572 'from' => new TitleValue( 0, 'SomeDbKey' ),
1573 'sort' => WatchedItemQueryService
::SORT_DESC
,
1575 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1576 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1580 'until' => new TitleValue( 0, 'SomeDbKey' ),
1581 'sort' => WatchedItemQueryService
::SORT_ASC
1583 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1584 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1588 'until' => new TitleValue( 0, 'SomeDbKey' ),
1589 'sort' => WatchedItemQueryService
::SORT_DESC
1591 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1592 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1596 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1597 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1598 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1599 'sort' => WatchedItemQueryService
::SORT_ASC
1602 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1603 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1604 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1606 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1610 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1611 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1612 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1613 'sort' => WatchedItemQueryService
::SORT_DESC
1616 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1617 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1618 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1620 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1626 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1628 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1630 array $expectedConds,
1631 array $expectedDbOptions
1633 $user = $this->getMockNonAnonUserWithId( 1 );
1635 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1637 $mockDb = $this->getMockDb();
1638 $mockDb->expects( $this->any() )
1639 ->method( 'addQuotes' )
1640 ->will( $this->returnCallback( function ( $value ) {
1643 $mockDb->expects( $this->any() )
1644 ->method( 'makeList' )
1646 $this->isType( 'array' ),
1647 $this->isType( 'int' )
1649 ->will( $this->returnCallback( function ( $a, $conj ) {
1650 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1651 return implode( $sqlConj, array_map( function ( $s ) {
1652 return '(' . $s . ')';
1656 $mockDb->expects( $this->once() )
1657 ->method( 'select' )
1660 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1662 $this->isType( 'string' ),
1665 ->will( $this->returnValue( [] ) );
1667 $queryService = $this->newService( $mockDb );
1669 $items = $queryService->getWatchedItemsForUser( $user, $options );
1670 $this->assertEmpty( $items );
1673 public function getWatchedItemsForUserInvalidOptionsProvider() {
1676 [ 'sort' => 'foo' ],
1677 'Bad value for parameter $options[\'sort\']'
1680 [ 'filter' => 'foo' ],
1681 'Bad value for parameter $options[\'filter\']'
1684 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1685 'Bad value for parameter $options[\'sort\']: must be provided'
1688 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1689 'Bad value for parameter $options[\'sort\']: must be provided'
1692 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1693 'Bad value for parameter $options[\'sort\']: must be provided'
1699 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1701 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1703 $expectedInExceptionMessage
1705 $queryService = $this->newService( $this->getMockDb() );
1707 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1708 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1711 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1712 $mockDb = $this->getMockDb();
1714 $mockDb->expects( $this->never() )
1715 ->method( $this->anything() );
1717 $queryService = $this->newService( $mockDb );
1719 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1720 $this->assertEmpty( $items );