3 use Wikimedia\ScopedCallback
;
4 use Wikimedia\TestingAccessWrapper
;
7 * @covers WatchedItemQueryService
9 class WatchedItemQueryServiceUnitTest
extends PHPUnit_Framework_TestCase
{
12 * @return PHPUnit_Framework_MockObject_MockObject|Database
14 private function getMockDb() {
15 $mock = $this->getMockBuilder( Database
::class )
16 ->disableOriginalConstructor()
19 $mock->expects( $this->any() )
20 ->method( 'makeList' )
22 $this->isType( 'array' ),
23 $this->isType( 'int' )
25 ->will( $this->returnCallback( function ( $a, $conj ) {
26 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
27 return join( $sqlConj, array_map( function ( $s ) {
28 return '(' . $s . ')';
33 $mock->expects( $this->any() )
34 ->method( 'addQuotes' )
35 ->will( $this->returnCallback( function ( $value ) {
39 $mock->expects( $this->any() )
40 ->method( 'timestamp' )
41 ->will( $this->returnArgument( 0 ) );
43 $mock->expects( $this->any() )
45 ->willReturnCallback( function ( $a, $b ) {
53 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
54 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
56 private function getMockLoadBalancer( $mockDb ) {
57 $mock = $this->getMockBuilder( LoadBalancer
::class )
58 ->disableOriginalConstructor()
60 $mock->expects( $this->any() )
61 ->method( 'getConnectionRef' )
63 ->will( $this->returnValue( $mockDb ) );
69 * @return PHPUnit_Framework_MockObject_MockObject|User
71 private function getMockNonAnonUserWithId( $id ) {
72 $mock = $this->getMockBuilder( User
::class )->getMock();
73 $mock->expects( $this->any() )
75 ->will( $this->returnValue( false ) );
76 $mock->expects( $this->any() )
78 ->will( $this->returnValue( $id ) );
84 * @return PHPUnit_Framework_MockObject_MockObject|User
86 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
87 $mock = $this->getMockNonAnonUserWithId( $id );
88 $mock->expects( $this->any() )
89 ->method( 'isAllowed' )
90 ->will( $this->returnValue( true ) );
91 $mock->expects( $this->any() )
92 ->method( 'isAllowedAny' )
93 ->will( $this->returnValue( true ) );
94 $mock->expects( $this->any() )
95 ->method( 'useRCPatrol' )
96 ->will( $this->returnValue( true ) );
102 * @param string $notAllowedAction
103 * @return PHPUnit_Framework_MockObject_MockObject|User
105 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
106 $mock = $this->getMockNonAnonUserWithId( $id );
108 $mock->expects( $this->any() )
109 ->method( 'isAllowed' )
110 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
111 return $action !== $notAllowedAction;
113 $mock->expects( $this->any() )
114 ->method( 'isAllowedAny' )
115 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
116 $actions = func_get_args();
117 return !in_array( $notAllowedAction, $actions );
125 * @return PHPUnit_Framework_MockObject_MockObject|User
127 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
128 $mock = $this->getMockNonAnonUserWithId( $id );
130 $mock->expects( $this->any() )
131 ->method( 'isAllowed' )
132 ->will( $this->returnValue( true ) );
133 $mock->expects( $this->any() )
134 ->method( 'isAllowedAny' )
135 ->will( $this->returnValue( true ) );
137 $mock->expects( $this->any() )
138 ->method( 'useRCPatrol' )
139 ->will( $this->returnValue( false ) );
140 $mock->expects( $this->any() )
141 ->method( 'useNPPatrol' )
142 ->will( $this->returnValue( false ) );
147 private function getMockAnonUser() {
148 $mock = $this->getMockBuilder( User
::class )->getMock();
149 $mock->expects( $this->any() )
151 ->will( $this->returnValue( true ) );
155 private function getFakeRow( array $rowValues ) {
156 $fakeRow = new stdClass();
157 foreach ( $rowValues as $valueName => $value ) {
158 $fakeRow->$valueName = $value;
163 public function testGetWatchedItemsWithRecentChangeInfo() {
164 $mockDb = $this->getMockDb();
165 $mockDb->expects( $this->once() )
168 [ 'recentchanges', 'watchlist', 'page' ],
176 'wl_notificationtimestamp',
183 '(rc_this_oldid=page_latest) OR (rc_type=3)',
185 $this->isType( 'string' ),
193 'wl_namespace=rc_namespace',
203 ->will( $this->returnValue( [
207 'rc_title' => 'Foo1',
208 'rc_timestamp' => '20151212010101',
211 'wl_notificationtimestamp' => '20151212010101',
216 'rc_title' => 'Foo2',
217 'rc_timestamp' => '20151212010102',
220 'wl_notificationtimestamp' => null,
225 'rc_title' => 'Foo3',
226 'rc_timestamp' => '20151212010103',
229 'wl_notificationtimestamp' => null,
233 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
234 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
237 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
238 $user, [ 'limit' => 2 ], $startFrom
241 $this->assertInternalType( 'array', $items );
242 $this->assertCount( 2, $items );
244 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
245 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
246 $this->assertInternalType( 'array', $recentChangeInfo );
250 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
257 'rc_title' => 'Foo1',
258 'rc_timestamp' => '20151212010101',
266 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
273 'rc_title' => 'Foo2',
274 'rc_timestamp' => '20151212010102',
281 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
284 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
285 $mockDb = $this->getMockDb();
286 $mockDb->expects( $this->once() )
289 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
297 'wl_notificationtimestamp',
301 'extension_dummy_field',
305 '(rc_this_oldid=page_latest) OR (rc_type=3)',
306 'extension_dummy_cond',
308 $this->isType( 'string' ),
310 'extension_dummy_option',
316 'wl_namespace=rc_namespace',
324 'extension_dummy_join_cond' => [],
327 ->will( $this->returnValue( [
331 'rc_title' => 'Foo1',
332 'rc_timestamp' => '20151212010101',
335 'wl_notificationtimestamp' => '20151212010101',
340 'rc_title' => 'Foo2',
341 'rc_timestamp' => '20151212010102',
344 'wl_notificationtimestamp' => null,
348 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
350 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
352 $mockExtension->expects( $this->once() )
353 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
355 $this->identicalTo( $user ),
356 $this->isType( 'array' ),
357 $this->isInstanceOf( IDatabase
::class ),
358 $this->isType( 'array' ),
359 $this->isType( 'array' ),
360 $this->isType( 'array' ),
361 $this->isType( 'array' ),
362 $this->isType( 'array' )
364 ->will( $this->returnCallback( function (
365 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
367 $tables[] = 'extension_dummy_table';
368 $fields[] = 'extension_dummy_field';
369 $conds[] = 'extension_dummy_cond';
370 $dbOptions[] = 'extension_dummy_option';
371 $joinConds['extension_dummy_join_cond'] = [];
373 $mockExtension->expects( $this->once() )
374 ->method( 'modifyWatchedItemsWithRCInfo' )
376 $this->identicalTo( $user ),
377 $this->isType( 'array' ),
378 $this->isInstanceOf( IDatabase
::class ),
379 $this->isType( 'array' ),
381 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
383 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
384 foreach ( $items as $i => &$item ) {
385 $item[1]['extension_dummy_field'] = $i;
389 $this->assertNull( $startFrom );
390 $startFrom = [ '20160203123456', 42 ];
393 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
394 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
397 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
398 $user, [], $startFrom
401 $this->assertInternalType( 'array', $items );
402 $this->assertCount( 2, $items );
404 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
405 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
406 $this->assertInternalType( 'array', $recentChangeInfo );
410 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
417 'rc_title' => 'Foo1',
418 'rc_timestamp' => '20151212010101',
421 'extension_dummy_field' => 0,
427 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
434 'rc_title' => 'Foo2',
435 'rc_timestamp' => '20151212010102',
438 'extension_dummy_field' => 1,
443 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
446 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
449 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
452 [ 'rc_type', 'rc_minor', 'rc_bot' ],
458 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
467 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
476 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
480 'rc_comment_text' => 'rc_comment',
481 'rc_comment_data' => 'NULL',
482 'rc_comment_cid' => 'NULL',
487 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD
],
490 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
492 [ 'comment_rc_comment' => 'comment' ],
494 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
495 'rc_comment_data' => 'comment_rc_comment.comment_data',
496 'rc_comment_cid' => 'comment_rc_comment.comment_id',
500 [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
501 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH
],
504 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
506 [ 'comment_rc_comment' => 'comment' ],
508 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
509 'rc_comment_data' => 'comment_rc_comment.comment_data',
510 'rc_comment_cid' => 'comment_rc_comment.comment_id',
514 [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
515 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW
],
518 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
520 [ 'comment_rc_comment' => 'comment' ],
522 'rc_comment_text' => 'comment_rc_comment.comment_text',
523 'rc_comment_data' => 'comment_rc_comment.comment_data',
524 'rc_comment_cid' => 'comment_rc_comment.comment_id',
528 [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
529 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW
],
532 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
535 [ 'rc_patrolled', 'rc_log_type' ],
541 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
544 [ 'rc_old_len', 'rc_new_len' ],
550 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
553 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
559 [ 'namespaceIds' => [ 0, 1 ] ],
563 [ 'wl_namespace' => [ 0, 1 ] ],
568 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
572 [ 'wl_namespace' => [ 0, 1 ] ],
577 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
581 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
586 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
591 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
595 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
600 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
604 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
608 [ "rc_timestamp <= '20151212010101'" ],
609 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
613 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
617 [ "rc_timestamp >= '20151212010101'" ],
618 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
623 'dir' => WatchedItemQueryService
::DIR_OLDER
,
624 'start' => '20151212020101',
625 'end' => '20151212010101'
630 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
631 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
635 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
639 [ "rc_timestamp >= '20151212010101'" ],
640 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
644 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
648 [ "rc_timestamp <= '20151212010101'" ],
649 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
654 'dir' => WatchedItemQueryService
::DIR_NEWER
,
655 'start' => '20151212010101',
656 'end' => '20151212020101'
661 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
662 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
675 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
684 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
693 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
702 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
711 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
720 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
729 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
738 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
742 [ 'rc_patrolled != 0' ],
747 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
751 [ 'rc_patrolled = 0' ],
756 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
760 [ 'rc_timestamp >= wl_notificationtimestamp' ],
765 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
769 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
774 [ 'onlyByUser' => 'SomeOtherUser' ],
778 [ 'rc_user_text' => 'SomeOtherUser' ],
783 [ 'notByUser' => 'SomeOtherUser' ],
787 [ "rc_user_text != 'SomeOtherUser'" ],
792 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
793 [ '20151212010101', 123 ],
797 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
799 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
803 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
804 [ '20151212010101', 123 ],
808 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
810 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
814 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
815 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
819 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
821 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
828 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
830 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
833 array $expectedExtraTables,
834 array $expectedExtraFields,
835 array $expectedExtraConds,
836 array $expectedDbOptions,
837 array $expectedExtraJoinConds,
840 // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals().
843 foreach ( $globals as $k => $v ) {
844 $resetGlobals[$k] = $GLOBALS[$k];
847 $reset = new ScopedCallback( function () use ( $resetGlobals ) {
848 foreach ( $resetGlobals as $k => $v ) {
854 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
855 $expectedFields = array_merge(
863 'wl_notificationtimestamp',
871 $expectedConds = array_merge(
872 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
875 $expectedJoinConds = array_merge(
880 'wl_namespace=rc_namespace',
889 $expectedExtraJoinConds
892 $mockDb = $this->getMockDb();
893 $mockDb->expects( $this->once() )
899 $this->isType( 'string' ),
903 ->will( $this->returnValue( [] ) );
905 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
906 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
908 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
910 $this->assertEmpty( $items );
911 $this->assertNull( $startFrom );
914 public function filterPatrolledOptionProvider() {
916 [ WatchedItemQueryService
::FILTER_PATROLLED
],
917 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
922 * @dataProvider filterPatrolledOptionProvider
924 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
927 $mockDb = $this->getMockDb();
928 $mockDb->expects( $this->once() )
931 [ 'recentchanges', 'watchlist', 'page' ],
932 $this->isType( 'array' ),
933 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
934 $this->isType( 'string' ),
935 $this->isType( 'array' ),
936 $this->isType( 'array' )
938 ->will( $this->returnValue( [] ) );
940 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
942 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
943 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
945 [ 'filters' => [ $filtersOption ] ]
948 $this->assertEmpty( $items );
951 public function mysqlIndexOptimizationProvider() {
956 [ "rc_timestamp > ''" ],
960 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
961 [ "rc_timestamp <= '20151212010101'" ],
965 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
966 [ "rc_timestamp >= '20151212010101'" ],
977 * @dataProvider mysqlIndexOptimizationProvider
979 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
982 array $expectedExtraConds
984 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
985 $conds = array_merge( $commonConds, $expectedExtraConds );
987 $mockDb = $this->getMockDb();
988 $mockDb->expects( $this->once() )
991 [ 'recentchanges', 'watchlist', 'page' ],
992 $this->isType( 'array' ),
994 $this->isType( 'string' ),
995 $this->isType( 'array' ),
996 $this->isType( 'array' )
998 ->will( $this->returnValue( [] ) );
999 $mockDb->expects( $this->any() )
1000 ->method( 'getType' )
1001 ->will( $this->returnValue( $dbType ) );
1003 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1004 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1006 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1008 $this->assertEmpty( $items );
1011 public function userPermissionRelatedExtraChecksProvider() {
1017 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1018 LogPage
::DELETED_ACTION
. ')'
1025 '(rc_type != ' . RC_LOG
. ') OR (' .
1026 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1027 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1034 '(rc_type != ' . RC_LOG
. ') OR (' .
1035 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1036 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1040 [ 'onlyByUser' => 'SomeOtherUser' ],
1043 'rc_user_text' => 'SomeOtherUser',
1044 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1045 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1046 LogPage
::DELETED_ACTION
. ')'
1050 [ 'onlyByUser' => 'SomeOtherUser' ],
1053 'rc_user_text' => 'SomeOtherUser',
1054 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1055 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1056 '(rc_type != ' . RC_LOG
. ') OR (' .
1057 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1058 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1062 [ 'onlyByUser' => 'SomeOtherUser' ],
1065 'rc_user_text' => 'SomeOtherUser',
1066 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1067 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1068 '(rc_type != ' . RC_LOG
. ') OR (' .
1069 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1070 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1077 * @dataProvider userPermissionRelatedExtraChecksProvider
1079 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1082 array $expectedExtraConds
1084 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1085 $conds = array_merge( $commonConds, $expectedExtraConds );
1087 $mockDb = $this->getMockDb();
1088 $mockDb->expects( $this->once() )
1089 ->method( 'select' )
1091 [ 'recentchanges', 'watchlist', 'page' ],
1092 $this->isType( 'array' ),
1094 $this->isType( 'string' ),
1095 $this->isType( 'array' ),
1096 $this->isType( 'array' )
1098 ->will( $this->returnValue( [] ) );
1100 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1102 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1103 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1105 $this->assertEmpty( $items );
1108 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1109 $mockDb = $this->getMockDb();
1110 $mockDb->expects( $this->once() )
1111 ->method( 'select' )
1113 [ 'recentchanges', 'watchlist' ],
1121 'wl_notificationtimestamp',
1127 [ 'wl_user' => 1, ],
1128 $this->isType( 'string' ),
1134 'wl_namespace=rc_namespace',
1140 ->will( $this->returnValue( [] ) );
1142 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1143 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1145 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1147 $this->assertEmpty( $items );
1150 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1153 [ 'rcTypes' => [ 1337 ] ],
1155 'Bad value for parameter $options[\'rcTypes\']',
1158 [ 'rcTypes' => [ 'edit' ] ],
1160 'Bad value for parameter $options[\'rcTypes\']',
1163 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1165 'Bad value for parameter $options[\'rcTypes\']',
1170 'Bad value for parameter $options[\'dir\']',
1173 [ 'start' => '20151212010101' ],
1175 'Bad value for parameter $options[\'dir\']: must be provided',
1178 [ 'end' => '20151212010101' ],
1180 'Bad value for parameter $options[\'dir\']: must be provided',
1184 [ '20151212010101', 123 ],
1185 'Bad value for parameter $options[\'dir\']: must be provided',
1188 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1190 'Bad value for parameter $startFrom: must be a two-element array',
1193 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1194 [ '20151212010101' ],
1195 'Bad value for parameter $startFrom: must be a two-element array',
1198 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1199 [ '20151212010101', 123, 'foo' ],
1200 'Bad value for parameter $startFrom: must be a two-element array',
1203 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1205 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1208 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1210 'Bad value for parameter $options[\'watchlistOwner\']',
1216 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1218 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1221 $expectedInExceptionMessage
1223 $mockDb = $this->getMockDb();
1224 $mockDb->expects( $this->never() )
1225 ->method( $this->anything() );
1227 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1228 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1230 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1231 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1234 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1235 $mockDb = $this->getMockDb();
1236 $mockDb->expects( $this->once() )
1237 ->method( 'select' )
1239 [ 'recentchanges', 'watchlist', 'page' ],
1247 'wl_notificationtimestamp',
1250 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1251 $this->isType( 'string' ),
1257 'wl_namespace=rc_namespace',
1263 'rc_cur_id=page_id',
1267 ->will( $this->returnValue( [] ) );
1269 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1270 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1272 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1274 [ 'usedInGenerator' => true ]
1277 $this->assertEmpty( $items );
1280 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1281 $mockDb = $this->getMockDb();
1282 $mockDb->expects( $this->once() )
1283 ->method( 'select' )
1285 [ 'recentchanges', 'watchlist' ],
1293 'wl_notificationtimestamp',
1297 $this->isType( 'string' ),
1303 'wl_namespace=rc_namespace',
1309 ->will( $this->returnValue( [] ) );
1311 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1312 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1314 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1316 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1319 $this->assertEmpty( $items );
1322 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1323 $mockDb = $this->getMockDb();
1324 $mockDb->expects( $this->once() )
1325 ->method( 'select' )
1327 $this->isType( 'array' ),
1328 $this->isType( 'array' ),
1331 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1333 $this->isType( 'string' ),
1334 $this->isType( 'array' ),
1335 $this->isType( 'array' )
1337 ->will( $this->returnValue( [] ) );
1339 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1340 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1341 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1342 $otherUser->expects( $this->once() )
1343 ->method( 'getOption' )
1344 ->with( 'watchlisttoken' )
1345 ->willReturn( '0123456789abcdef' );
1347 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1349 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1352 $this->assertEmpty( $items );
1355 public function invalidWatchlistTokenProvider() {
1363 * @dataProvider invalidWatchlistTokenProvider
1365 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1366 $mockDb = $this->getMockDb();
1367 $mockDb->expects( $this->never() )
1368 ->method( $this->anything() );
1370 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1371 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1372 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1373 $otherUser->expects( $this->once() )
1374 ->method( 'getOption' )
1375 ->with( 'watchlisttoken' )
1376 ->willReturn( '0123456789abcdef' );
1378 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1379 $queryService->getWatchedItemsWithRecentChangeInfo(
1381 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1385 public function testGetWatchedItemsForUser() {
1386 $mockDb = $this->getMockDb();
1387 $mockDb->expects( $this->once() )
1388 ->method( 'select' )
1391 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1394 ->will( $this->returnValue( [
1395 $this->getFakeRow( [
1396 'wl_namespace' => 0,
1397 'wl_title' => 'Foo1',
1398 'wl_notificationtimestamp' => '20151212010101',
1400 $this->getFakeRow( [
1401 'wl_namespace' => 1,
1402 'wl_title' => 'Foo2',
1403 'wl_notificationtimestamp' => null,
1407 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1408 $user = $this->getMockNonAnonUserWithId( 1 );
1410 $items = $queryService->getWatchedItemsForUser( $user );
1412 $this->assertInternalType( 'array', $items );
1413 $this->assertCount( 2, $items );
1414 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1415 $this->assertEquals(
1416 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1419 $this->assertEquals(
1420 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1425 public function provideGetWatchedItemsForUserOptions() {
1428 [ 'namespaceIds' => [ 0, 1 ], ],
1429 [ 'wl_namespace' => [ 0, 1 ], ],
1433 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1435 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1439 'namespaceIds' => [ 0 ],
1440 'sort' => WatchedItemQueryService
::SORT_ASC
,
1442 [ 'wl_namespace' => [ 0 ], ],
1443 [ 'ORDER BY' => 'wl_title ASC' ]
1452 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1453 'limit' => "10; DROP TABLE watchlist;\n--",
1455 [ 'wl_namespace' => [ 0, 1 ], ],
1459 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1460 [ 'wl_notificationtimestamp IS NOT NULL' ],
1464 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1465 [ 'wl_notificationtimestamp IS NULL' ],
1469 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1471 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1475 'namespaceIds' => [ 0 ],
1476 'sort' => WatchedItemQueryService
::SORT_DESC
,
1478 [ 'wl_namespace' => [ 0 ], ],
1479 [ 'ORDER BY' => 'wl_title DESC' ]
1485 * @dataProvider provideGetWatchedItemsForUserOptions
1487 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1489 array $expectedConds,
1490 array $expectedDbOptions
1492 $mockDb = $this->getMockDb();
1493 $user = $this->getMockNonAnonUserWithId( 1 );
1495 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1496 $mockDb->expects( $this->once() )
1497 ->method( 'select' )
1500 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1502 $this->isType( 'string' ),
1505 ->will( $this->returnValue( [] ) );
1507 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1509 $items = $queryService->getWatchedItemsForUser( $user, $options );
1510 $this->assertEmpty( $items );
1513 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1517 'from' => new TitleValue( 0, 'SomeDbKey' ),
1518 'sort' => WatchedItemQueryService
::SORT_ASC
1520 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1521 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1525 'from' => new TitleValue( 0, 'SomeDbKey' ),
1526 'sort' => WatchedItemQueryService
::SORT_DESC
,
1528 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1529 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1533 'until' => new TitleValue( 0, 'SomeDbKey' ),
1534 'sort' => WatchedItemQueryService
::SORT_ASC
1536 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1537 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1541 'until' => new TitleValue( 0, 'SomeDbKey' ),
1542 'sort' => WatchedItemQueryService
::SORT_DESC
1544 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1545 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1549 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1550 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1551 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1552 'sort' => WatchedItemQueryService
::SORT_ASC
1555 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1556 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1557 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1559 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1563 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1564 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1565 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1566 'sort' => WatchedItemQueryService
::SORT_DESC
1569 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1570 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1571 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1573 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1579 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1581 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1583 array $expectedConds,
1584 array $expectedDbOptions
1586 $user = $this->getMockNonAnonUserWithId( 1 );
1588 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1590 $mockDb = $this->getMockDb();
1591 $mockDb->expects( $this->any() )
1592 ->method( 'addQuotes' )
1593 ->will( $this->returnCallback( function ( $value ) {
1596 $mockDb->expects( $this->any() )
1597 ->method( 'makeList' )
1599 $this->isType( 'array' ),
1600 $this->isType( 'int' )
1602 ->will( $this->returnCallback( function ( $a, $conj ) {
1603 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1604 return join( $sqlConj, array_map( function ( $s ) {
1605 return '(' . $s . ')';
1609 $mockDb->expects( $this->once() )
1610 ->method( 'select' )
1613 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1615 $this->isType( 'string' ),
1618 ->will( $this->returnValue( [] ) );
1620 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1622 $items = $queryService->getWatchedItemsForUser( $user, $options );
1623 $this->assertEmpty( $items );
1626 public function getWatchedItemsForUserInvalidOptionsProvider() {
1629 [ 'sort' => 'foo' ],
1630 'Bad value for parameter $options[\'sort\']'
1633 [ 'filter' => 'foo' ],
1634 'Bad value for parameter $options[\'filter\']'
1637 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1638 'Bad value for parameter $options[\'sort\']: must be provided'
1641 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1642 'Bad value for parameter $options[\'sort\']: must be provided'
1645 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1646 'Bad value for parameter $options[\'sort\']: must be provided'
1652 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1654 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1656 $expectedInExceptionMessage
1658 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
1660 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1661 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1664 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1665 $mockDb = $this->getMockDb();
1667 $mockDb->expects( $this->never() )
1668 ->method( $this->anything() );
1670 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1672 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1673 $this->assertEmpty( $items );