3 use Wikimedia\ScopedCallback
;
4 use Wikimedia\TestingAccessWrapper
;
7 * @covers WatchedItemQueryService
9 class WatchedItemQueryServiceUnitTest
extends PHPUnit_Framework_TestCase
{
11 use MediaWikiCoversValidator
;
14 * @return PHPUnit_Framework_MockObject_MockObject|Database
16 private function getMockDb() {
17 $mock = $this->getMockBuilder( Database
::class )
18 ->disableOriginalConstructor()
21 $mock->expects( $this->any() )
22 ->method( 'makeList' )
24 $this->isType( 'array' ),
25 $this->isType( 'int' )
27 ->will( $this->returnCallback( function ( $a, $conj ) {
28 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
29 return join( $sqlConj, array_map( function ( $s ) {
30 return '(' . $s . ')';
35 $mock->expects( $this->any() )
36 ->method( 'addQuotes' )
37 ->will( $this->returnCallback( function ( $value ) {
41 $mock->expects( $this->any() )
42 ->method( 'timestamp' )
43 ->will( $this->returnArgument( 0 ) );
45 $mock->expects( $this->any() )
47 ->willReturnCallback( function ( $a, $b ) {
55 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
56 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
58 private function getMockLoadBalancer( $mockDb ) {
59 $mock = $this->getMockBuilder( LoadBalancer
::class )
60 ->disableOriginalConstructor()
62 $mock->expects( $this->any() )
63 ->method( 'getConnectionRef' )
65 ->will( $this->returnValue( $mockDb ) );
71 * @return PHPUnit_Framework_MockObject_MockObject|User
73 private function getMockNonAnonUserWithId( $id ) {
74 $mock = $this->getMockBuilder( User
::class )->getMock();
75 $mock->expects( $this->any() )
77 ->will( $this->returnValue( false ) );
78 $mock->expects( $this->any() )
80 ->will( $this->returnValue( $id ) );
86 * @return PHPUnit_Framework_MockObject_MockObject|User
88 private function getMockUnrestrictedNonAnonUserWithId( $id ) {
89 $mock = $this->getMockNonAnonUserWithId( $id );
90 $mock->expects( $this->any() )
91 ->method( 'isAllowed' )
92 ->will( $this->returnValue( true ) );
93 $mock->expects( $this->any() )
94 ->method( 'isAllowedAny' )
95 ->will( $this->returnValue( true ) );
96 $mock->expects( $this->any() )
97 ->method( 'useRCPatrol' )
98 ->will( $this->returnValue( true ) );
104 * @param string $notAllowedAction
105 * @return PHPUnit_Framework_MockObject_MockObject|User
107 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
108 $mock = $this->getMockNonAnonUserWithId( $id );
110 $mock->expects( $this->any() )
111 ->method( 'isAllowed' )
112 ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
113 return $action !== $notAllowedAction;
115 $mock->expects( $this->any() )
116 ->method( 'isAllowedAny' )
117 ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
118 $actions = func_get_args();
119 return !in_array( $notAllowedAction, $actions );
127 * @return PHPUnit_Framework_MockObject_MockObject|User
129 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
130 $mock = $this->getMockNonAnonUserWithId( $id );
132 $mock->expects( $this->any() )
133 ->method( 'isAllowed' )
134 ->will( $this->returnValue( true ) );
135 $mock->expects( $this->any() )
136 ->method( 'isAllowedAny' )
137 ->will( $this->returnValue( true ) );
139 $mock->expects( $this->any() )
140 ->method( 'useRCPatrol' )
141 ->will( $this->returnValue( false ) );
142 $mock->expects( $this->any() )
143 ->method( 'useNPPatrol' )
144 ->will( $this->returnValue( false ) );
149 private function getMockAnonUser() {
150 $mock = $this->getMockBuilder( User
::class )->getMock();
151 $mock->expects( $this->any() )
153 ->will( $this->returnValue( true ) );
157 private function getFakeRow( array $rowValues ) {
158 $fakeRow = new stdClass();
159 foreach ( $rowValues as $valueName => $value ) {
160 $fakeRow->$valueName = $value;
165 public function testGetWatchedItemsWithRecentChangeInfo() {
166 $mockDb = $this->getMockDb();
167 $mockDb->expects( $this->once() )
170 [ 'recentchanges', 'watchlist', 'page' ],
178 'wl_notificationtimestamp',
185 '(rc_this_oldid=page_latest) OR (rc_type=3)',
187 $this->isType( 'string' ),
195 'wl_namespace=rc_namespace',
205 ->will( $this->returnValue( [
209 'rc_title' => 'Foo1',
210 'rc_timestamp' => '20151212010101',
213 'wl_notificationtimestamp' => '20151212010101',
218 'rc_title' => 'Foo2',
219 'rc_timestamp' => '20151212010102',
222 'wl_notificationtimestamp' => null,
227 'rc_title' => 'Foo3',
228 'rc_timestamp' => '20151212010103',
231 'wl_notificationtimestamp' => null,
235 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
236 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
239 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
240 $user, [ 'limit' => 2 ], $startFrom
243 $this->assertInternalType( 'array', $items );
244 $this->assertCount( 2, $items );
246 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
247 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
248 $this->assertInternalType( 'array', $recentChangeInfo );
252 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
259 'rc_title' => 'Foo1',
260 'rc_timestamp' => '20151212010101',
268 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
275 'rc_title' => 'Foo2',
276 'rc_timestamp' => '20151212010102',
283 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
286 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
287 $mockDb = $this->getMockDb();
288 $mockDb->expects( $this->once() )
291 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
299 'wl_notificationtimestamp',
303 'extension_dummy_field',
307 '(rc_this_oldid=page_latest) OR (rc_type=3)',
308 'extension_dummy_cond',
310 $this->isType( 'string' ),
312 'extension_dummy_option',
318 'wl_namespace=rc_namespace',
326 'extension_dummy_join_cond' => [],
329 ->will( $this->returnValue( [
333 'rc_title' => 'Foo1',
334 'rc_timestamp' => '20151212010101',
337 'wl_notificationtimestamp' => '20151212010101',
342 'rc_title' => 'Foo2',
343 'rc_timestamp' => '20151212010102',
346 'wl_notificationtimestamp' => null,
350 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
352 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension
::class )
354 $mockExtension->expects( $this->once() )
355 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
357 $this->identicalTo( $user ),
358 $this->isType( 'array' ),
359 $this->isInstanceOf( IDatabase
::class ),
360 $this->isType( 'array' ),
361 $this->isType( 'array' ),
362 $this->isType( 'array' ),
363 $this->isType( 'array' ),
364 $this->isType( 'array' )
366 ->will( $this->returnCallback( function (
367 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
369 $tables[] = 'extension_dummy_table';
370 $fields[] = 'extension_dummy_field';
371 $conds[] = 'extension_dummy_cond';
372 $dbOptions[] = 'extension_dummy_option';
373 $joinConds['extension_dummy_join_cond'] = [];
375 $mockExtension->expects( $this->once() )
376 ->method( 'modifyWatchedItemsWithRCInfo' )
378 $this->identicalTo( $user ),
379 $this->isType( 'array' ),
380 $this->isInstanceOf( IDatabase
::class ),
381 $this->isType( 'array' ),
383 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
385 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
386 foreach ( $items as $i => &$item ) {
387 $item[1]['extension_dummy_field'] = $i;
391 $this->assertNull( $startFrom );
392 $startFrom = [ '20160203123456', 42 ];
395 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
396 TestingAccessWrapper
::newFromObject( $queryService )->extensions
= [ $mockExtension ];
399 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
400 $user, [], $startFrom
403 $this->assertInternalType( 'array', $items );
404 $this->assertCount( 2, $items );
406 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
407 $this->assertInstanceOf( WatchedItem
::class, $watchedItem );
408 $this->assertInternalType( 'array', $recentChangeInfo );
412 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
419 'rc_title' => 'Foo1',
420 'rc_timestamp' => '20151212010101',
423 'extension_dummy_field' => 0,
429 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
436 'rc_title' => 'Foo2',
437 'rc_timestamp' => '20151212010102',
440 'extension_dummy_field' => 1,
445 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
448 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
451 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_FLAGS
] ],
454 [ 'rc_type', 'rc_minor', 'rc_bot' ],
460 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER
] ],
469 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_USER_ID
] ],
478 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
482 'rc_comment_text' => 'rc_comment',
483 'rc_comment_data' => 'NULL',
484 'rc_comment_cid' => 'NULL',
489 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD
],
492 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
494 [ 'comment_rc_comment' => 'comment' ],
496 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
497 'rc_comment_data' => 'comment_rc_comment.comment_data',
498 'rc_comment_cid' => 'comment_rc_comment.comment_id',
502 [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
503 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH
],
506 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
508 [ 'comment_rc_comment' => 'comment' ],
510 'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
511 'rc_comment_data' => 'comment_rc_comment.comment_data',
512 'rc_comment_cid' => 'comment_rc_comment.comment_id',
516 [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
517 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW
],
520 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_COMMENT
] ],
522 [ 'comment_rc_comment' => 'comment' ],
524 'rc_comment_text' => 'comment_rc_comment.comment_text',
525 'rc_comment_data' => 'comment_rc_comment.comment_data',
526 'rc_comment_cid' => 'comment_rc_comment.comment_id',
530 [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
531 [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW
],
534 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_PATROL_INFO
] ],
537 [ 'rc_patrolled', 'rc_log_type' ],
543 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_SIZES
] ],
546 [ 'rc_old_len', 'rc_new_len' ],
552 [ 'includeFields' => [ WatchedItemQueryService
::INCLUDE_LOG_INFO
] ],
555 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
561 [ 'namespaceIds' => [ 0, 1 ] ],
565 [ 'wl_namespace' => [ 0, 1 ] ],
570 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
574 [ 'wl_namespace' => [ 0, 1 ] ],
579 [ 'rcTypes' => [ RC_EDIT
, RC_NEW
] ],
583 [ 'rc_type' => [ RC_EDIT
, RC_NEW
] ],
588 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
593 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
597 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
602 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
606 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'start' => '20151212010101' ],
610 [ "rc_timestamp <= '20151212010101'" ],
611 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
615 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
, 'end' => '20151212010101' ],
619 [ "rc_timestamp >= '20151212010101'" ],
620 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
625 'dir' => WatchedItemQueryService
::DIR_OLDER
,
626 'start' => '20151212020101',
627 'end' => '20151212010101'
632 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
633 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
637 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'start' => '20151212010101' ],
641 [ "rc_timestamp >= '20151212010101'" ],
642 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
646 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
, 'end' => '20151212010101' ],
650 [ "rc_timestamp <= '20151212010101'" ],
651 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
656 'dir' => WatchedItemQueryService
::DIR_NEWER
,
657 'start' => '20151212010101',
658 'end' => '20151212020101'
663 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
664 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
677 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
686 [ 'filters' => [ WatchedItemQueryService
::FILTER_MINOR
] ],
695 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_MINOR
] ],
704 [ 'filters' => [ WatchedItemQueryService
::FILTER_BOT
] ],
713 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_BOT
] ],
722 [ 'filters' => [ WatchedItemQueryService
::FILTER_ANON
] ],
731 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_ANON
] ],
740 [ 'filters' => [ WatchedItemQueryService
::FILTER_PATROLLED
] ],
744 [ 'rc_patrolled != 0' ],
749 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
] ],
753 [ 'rc_patrolled = 0' ],
758 [ 'filters' => [ WatchedItemQueryService
::FILTER_UNREAD
] ],
762 [ 'rc_timestamp >= wl_notificationtimestamp' ],
767 [ 'filters' => [ WatchedItemQueryService
::FILTER_NOT_UNREAD
] ],
771 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
776 [ 'onlyByUser' => 'SomeOtherUser' ],
780 [ 'rc_user_text' => 'SomeOtherUser' ],
785 [ 'notByUser' => 'SomeOtherUser' ],
789 [ "rc_user_text != 'SomeOtherUser'" ],
794 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
795 [ '20151212010101', 123 ],
799 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
801 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
805 [ 'dir' => WatchedItemQueryService
::DIR_NEWER
],
806 [ '20151212010101', 123 ],
810 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
812 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
816 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
817 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
821 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
823 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
830 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
832 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
835 array $expectedExtraTables,
836 array $expectedExtraFields,
837 array $expectedExtraConds,
838 array $expectedDbOptions,
839 array $expectedExtraJoinConds,
842 // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals().
845 foreach ( $globals as $k => $v ) {
846 $resetGlobals[$k] = $GLOBALS[$k];
849 $reset = new ScopedCallback( function () use ( $resetGlobals ) {
850 foreach ( $resetGlobals as $k => $v ) {
856 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
857 $expectedFields = array_merge(
865 'wl_notificationtimestamp',
873 $expectedConds = array_merge(
874 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
877 $expectedJoinConds = array_merge(
882 'wl_namespace=rc_namespace',
891 $expectedExtraJoinConds
894 $mockDb = $this->getMockDb();
895 $mockDb->expects( $this->once() )
901 $this->isType( 'string' ),
905 ->will( $this->returnValue( [] ) );
907 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
908 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
910 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
912 $this->assertEmpty( $items );
913 $this->assertNull( $startFrom );
916 public function filterPatrolledOptionProvider() {
918 [ WatchedItemQueryService
::FILTER_PATROLLED
],
919 [ WatchedItemQueryService
::FILTER_NOT_PATROLLED
],
924 * @dataProvider filterPatrolledOptionProvider
926 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
929 $mockDb = $this->getMockDb();
930 $mockDb->expects( $this->once() )
933 [ 'recentchanges', 'watchlist', 'page' ],
934 $this->isType( 'array' ),
935 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
936 $this->isType( 'string' ),
937 $this->isType( 'array' ),
938 $this->isType( 'array' )
940 ->will( $this->returnValue( [] ) );
942 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
944 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
945 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
947 [ 'filters' => [ $filtersOption ] ]
950 $this->assertEmpty( $items );
953 public function mysqlIndexOptimizationProvider() {
958 [ "rc_timestamp > ''" ],
962 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
963 [ "rc_timestamp <= '20151212010101'" ],
967 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService
::DIR_OLDER
],
968 [ "rc_timestamp >= '20151212010101'" ],
979 * @dataProvider mysqlIndexOptimizationProvider
981 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
984 array $expectedExtraConds
986 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
987 $conds = array_merge( $commonConds, $expectedExtraConds );
989 $mockDb = $this->getMockDb();
990 $mockDb->expects( $this->once() )
993 [ 'recentchanges', 'watchlist', 'page' ],
994 $this->isType( 'array' ),
996 $this->isType( 'string' ),
997 $this->isType( 'array' ),
998 $this->isType( 'array' )
1000 ->will( $this->returnValue( [] ) );
1001 $mockDb->expects( $this->any() )
1002 ->method( 'getType' )
1003 ->will( $this->returnValue( $dbType ) );
1005 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1006 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1008 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1010 $this->assertEmpty( $items );
1013 public function userPermissionRelatedExtraChecksProvider() {
1019 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1020 LogPage
::DELETED_ACTION
. ')'
1027 '(rc_type != ' . RC_LOG
. ') OR (' .
1028 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1029 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1036 '(rc_type != ' . RC_LOG
. ') OR (' .
1037 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1038 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1042 [ 'onlyByUser' => 'SomeOtherUser' ],
1045 'rc_user_text' => 'SomeOtherUser',
1046 '(rc_deleted & ' . Revision
::DELETED_USER
. ') != ' . Revision
::DELETED_USER
,
1047 '(rc_type != ' . RC_LOG
. ') OR ((rc_deleted & ' . LogPage
::DELETED_ACTION
. ') != ' .
1048 LogPage
::DELETED_ACTION
. ')'
1052 [ 'onlyByUser' => 'SomeOtherUser' ],
1055 'rc_user_text' => 'SomeOtherUser',
1056 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1057 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1058 '(rc_type != ' . RC_LOG
. ') OR (' .
1059 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1060 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1064 [ 'onlyByUser' => 'SomeOtherUser' ],
1067 'rc_user_text' => 'SomeOtherUser',
1068 '(rc_deleted & ' . ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
) . ') != ' .
1069 ( Revision
::DELETED_USER | Revision
::DELETED_RESTRICTED
),
1070 '(rc_type != ' . RC_LOG
. ') OR (' .
1071 '(rc_deleted & ' . ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ') != ' .
1072 ( LogPage
::DELETED_ACTION | LogPage
::DELETED_RESTRICTED
) . ')'
1079 * @dataProvider userPermissionRelatedExtraChecksProvider
1081 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1084 array $expectedExtraConds
1086 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1087 $conds = array_merge( $commonConds, $expectedExtraConds );
1089 $mockDb = $this->getMockDb();
1090 $mockDb->expects( $this->once() )
1091 ->method( 'select' )
1093 [ 'recentchanges', 'watchlist', 'page' ],
1094 $this->isType( 'array' ),
1096 $this->isType( 'string' ),
1097 $this->isType( 'array' ),
1098 $this->isType( 'array' )
1100 ->will( $this->returnValue( [] ) );
1102 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1104 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1105 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1107 $this->assertEmpty( $items );
1110 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1111 $mockDb = $this->getMockDb();
1112 $mockDb->expects( $this->once() )
1113 ->method( 'select' )
1115 [ 'recentchanges', 'watchlist' ],
1123 'wl_notificationtimestamp',
1129 [ 'wl_user' => 1, ],
1130 $this->isType( 'string' ),
1136 'wl_namespace=rc_namespace',
1142 ->will( $this->returnValue( [] ) );
1144 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1145 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1147 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1149 $this->assertEmpty( $items );
1152 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1155 [ 'rcTypes' => [ 1337 ] ],
1157 'Bad value for parameter $options[\'rcTypes\']',
1160 [ 'rcTypes' => [ 'edit' ] ],
1162 'Bad value for parameter $options[\'rcTypes\']',
1165 [ 'rcTypes' => [ RC_EDIT
, 1337 ] ],
1167 'Bad value for parameter $options[\'rcTypes\']',
1172 'Bad value for parameter $options[\'dir\']',
1175 [ 'start' => '20151212010101' ],
1177 'Bad value for parameter $options[\'dir\']: must be provided',
1180 [ 'end' => '20151212010101' ],
1182 'Bad value for parameter $options[\'dir\']: must be provided',
1186 [ '20151212010101', 123 ],
1187 'Bad value for parameter $options[\'dir\']: must be provided',
1190 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1192 'Bad value for parameter $startFrom: must be a two-element array',
1195 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1196 [ '20151212010101' ],
1197 'Bad value for parameter $startFrom: must be a two-element array',
1200 [ 'dir' => WatchedItemQueryService
::DIR_OLDER
],
1201 [ '20151212010101', 123, 'foo' ],
1202 'Bad value for parameter $startFrom: must be a two-element array',
1205 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1207 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1210 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1212 'Bad value for parameter $options[\'watchlistOwner\']',
1218 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1220 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1223 $expectedInExceptionMessage
1225 $mockDb = $this->getMockDb();
1226 $mockDb->expects( $this->never() )
1227 ->method( $this->anything() );
1229 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1230 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1232 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1233 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1236 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1237 $mockDb = $this->getMockDb();
1238 $mockDb->expects( $this->once() )
1239 ->method( 'select' )
1241 [ 'recentchanges', 'watchlist', 'page' ],
1249 'wl_notificationtimestamp',
1252 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1253 $this->isType( 'string' ),
1259 'wl_namespace=rc_namespace',
1265 'rc_cur_id=page_id',
1269 ->will( $this->returnValue( [] ) );
1271 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1272 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1274 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1276 [ 'usedInGenerator' => true ]
1279 $this->assertEmpty( $items );
1282 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1283 $mockDb = $this->getMockDb();
1284 $mockDb->expects( $this->once() )
1285 ->method( 'select' )
1287 [ 'recentchanges', 'watchlist' ],
1295 'wl_notificationtimestamp',
1299 $this->isType( 'string' ),
1305 'wl_namespace=rc_namespace',
1311 ->will( $this->returnValue( [] ) );
1313 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1314 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1316 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1318 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1321 $this->assertEmpty( $items );
1324 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1325 $mockDb = $this->getMockDb();
1326 $mockDb->expects( $this->once() )
1327 ->method( 'select' )
1329 $this->isType( 'array' ),
1330 $this->isType( 'array' ),
1333 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1335 $this->isType( 'string' ),
1336 $this->isType( 'array' ),
1337 $this->isType( 'array' )
1339 ->will( $this->returnValue( [] ) );
1341 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1342 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1343 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1344 $otherUser->expects( $this->once() )
1345 ->method( 'getOption' )
1346 ->with( 'watchlisttoken' )
1347 ->willReturn( '0123456789abcdef' );
1349 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1351 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1354 $this->assertEmpty( $items );
1357 public function invalidWatchlistTokenProvider() {
1365 * @dataProvider invalidWatchlistTokenProvider
1367 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1368 $mockDb = $this->getMockDb();
1369 $mockDb->expects( $this->never() )
1370 ->method( $this->anything() );
1372 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1373 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1374 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1375 $otherUser->expects( $this->once() )
1376 ->method( 'getOption' )
1377 ->with( 'watchlisttoken' )
1378 ->willReturn( '0123456789abcdef' );
1380 $this->setExpectedException( ApiUsageException
::class, 'Incorrect watchlist token provided' );
1381 $queryService->getWatchedItemsWithRecentChangeInfo(
1383 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1387 public function testGetWatchedItemsForUser() {
1388 $mockDb = $this->getMockDb();
1389 $mockDb->expects( $this->once() )
1390 ->method( 'select' )
1393 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1396 ->will( $this->returnValue( [
1397 $this->getFakeRow( [
1398 'wl_namespace' => 0,
1399 'wl_title' => 'Foo1',
1400 'wl_notificationtimestamp' => '20151212010101',
1402 $this->getFakeRow( [
1403 'wl_namespace' => 1,
1404 'wl_title' => 'Foo2',
1405 'wl_notificationtimestamp' => null,
1409 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1410 $user = $this->getMockNonAnonUserWithId( 1 );
1412 $items = $queryService->getWatchedItemsForUser( $user );
1414 $this->assertInternalType( 'array', $items );
1415 $this->assertCount( 2, $items );
1416 $this->assertContainsOnlyInstancesOf( WatchedItem
::class, $items );
1417 $this->assertEquals(
1418 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1421 $this->assertEquals(
1422 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1427 public function provideGetWatchedItemsForUserOptions() {
1430 [ 'namespaceIds' => [ 0, 1 ], ],
1431 [ 'wl_namespace' => [ 0, 1 ], ],
1435 [ 'sort' => WatchedItemQueryService
::SORT_ASC
, ],
1437 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1441 'namespaceIds' => [ 0 ],
1442 'sort' => WatchedItemQueryService
::SORT_ASC
,
1444 [ 'wl_namespace' => [ 0 ], ],
1445 [ 'ORDER BY' => 'wl_title ASC' ]
1454 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1455 'limit' => "10; DROP TABLE watchlist;\n--",
1457 [ 'wl_namespace' => [ 0, 1 ], ],
1461 [ 'filter' => WatchedItemQueryService
::FILTER_CHANGED
],
1462 [ 'wl_notificationtimestamp IS NOT NULL' ],
1466 [ 'filter' => WatchedItemQueryService
::FILTER_NOT_CHANGED
],
1467 [ 'wl_notificationtimestamp IS NULL' ],
1471 [ 'sort' => WatchedItemQueryService
::SORT_DESC
, ],
1473 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1477 'namespaceIds' => [ 0 ],
1478 'sort' => WatchedItemQueryService
::SORT_DESC
,
1480 [ 'wl_namespace' => [ 0 ], ],
1481 [ 'ORDER BY' => 'wl_title DESC' ]
1487 * @dataProvider provideGetWatchedItemsForUserOptions
1489 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1491 array $expectedConds,
1492 array $expectedDbOptions
1494 $mockDb = $this->getMockDb();
1495 $user = $this->getMockNonAnonUserWithId( 1 );
1497 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1498 $mockDb->expects( $this->once() )
1499 ->method( 'select' )
1502 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1504 $this->isType( 'string' ),
1507 ->will( $this->returnValue( [] ) );
1509 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1511 $items = $queryService->getWatchedItemsForUser( $user, $options );
1512 $this->assertEmpty( $items );
1515 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1519 'from' => new TitleValue( 0, 'SomeDbKey' ),
1520 'sort' => WatchedItemQueryService
::SORT_ASC
1522 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1523 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1527 'from' => new TitleValue( 0, 'SomeDbKey' ),
1528 'sort' => WatchedItemQueryService
::SORT_DESC
,
1530 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1531 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1535 'until' => new TitleValue( 0, 'SomeDbKey' ),
1536 'sort' => WatchedItemQueryService
::SORT_ASC
1538 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1539 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1543 'until' => new TitleValue( 0, 'SomeDbKey' ),
1544 'sort' => WatchedItemQueryService
::SORT_DESC
1546 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1547 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1551 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1552 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1553 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1554 'sort' => WatchedItemQueryService
::SORT_ASC
1557 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1558 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1559 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1561 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1565 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1566 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1567 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1568 'sort' => WatchedItemQueryService
::SORT_DESC
1571 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1572 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1573 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1575 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1581 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1583 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1585 array $expectedConds,
1586 array $expectedDbOptions
1588 $user = $this->getMockNonAnonUserWithId( 1 );
1590 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1592 $mockDb = $this->getMockDb();
1593 $mockDb->expects( $this->any() )
1594 ->method( 'addQuotes' )
1595 ->will( $this->returnCallback( function ( $value ) {
1598 $mockDb->expects( $this->any() )
1599 ->method( 'makeList' )
1601 $this->isType( 'array' ),
1602 $this->isType( 'int' )
1604 ->will( $this->returnCallback( function ( $a, $conj ) {
1605 $sqlConj = $conj === LIST_AND ?
' AND ' : ' OR ';
1606 return join( $sqlConj, array_map( function ( $s ) {
1607 return '(' . $s . ')';
1611 $mockDb->expects( $this->once() )
1612 ->method( 'select' )
1615 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1617 $this->isType( 'string' ),
1620 ->will( $this->returnValue( [] ) );
1622 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1624 $items = $queryService->getWatchedItemsForUser( $user, $options );
1625 $this->assertEmpty( $items );
1628 public function getWatchedItemsForUserInvalidOptionsProvider() {
1631 [ 'sort' => 'foo' ],
1632 'Bad value for parameter $options[\'sort\']'
1635 [ 'filter' => 'foo' ],
1636 'Bad value for parameter $options[\'filter\']'
1639 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1640 'Bad value for parameter $options[\'sort\']: must be provided'
1643 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1644 'Bad value for parameter $options[\'sort\']: must be provided'
1647 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1648 'Bad value for parameter $options[\'sort\']: must be provided'
1654 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1656 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1658 $expectedInExceptionMessage
1660 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
1662 $this->setExpectedException( InvalidArgumentException
::class, $expectedInExceptionMessage );
1663 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1666 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1667 $mockDb = $this->getMockDb();
1669 $mockDb->expects( $this->never() )
1670 ->method( $this->anything() );
1672 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1674 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1675 $this->assertEmpty( $items );