Merge "Add config for serving main Page from the domain root"
[lhc/web/wiklou.git] / tests / phpunit / includes / watcheditem / WatchedItemQueryServiceUnitTest.php
1 <?php
2
3 use MediaWiki\Permissions\PermissionManager;
4 use MediaWiki\User\UserIdentityValue;
5 use Wikimedia\Rdbms\IDatabase;
6 use Wikimedia\Rdbms\LoadBalancer;
7 use Wikimedia\TestingAccessWrapper;
8
9 /**
10 * @covers WatchedItemQueryService
11 */
12 class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
13
14 /**
15 * @return PHPUnit_Framework_MockObject_MockObject|CommentStore
16 */
17 private function getMockCommentStore() {
18 $mockStore = $this->getMockBuilder( CommentStore::class )
19 ->disableOriginalConstructor()
20 ->getMock();
21 $mockStore->expects( $this->any() )
22 ->method( 'getFields' )
23 ->willReturn( [ 'commentstore' => 'fields' ] );
24 $mockStore->expects( $this->any() )
25 ->method( 'getJoin' )
26 ->willReturn( [
27 'tables' => [ 'commentstore' => 'table' ],
28 'fields' => [ 'commentstore' => 'field' ],
29 'joins' => [ 'commentstore' => 'join' ],
30 ] );
31 return $mockStore;
32 }
33
34 /**
35 * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
36 */
37 private function getMockActorMigration() {
38 $mockStore = $this->getMockBuilder( ActorMigration::class )
39 ->disableOriginalConstructor()
40 ->getMock();
41 $mockStore->expects( $this->any() )
42 ->method( 'getJoin' )
43 ->willReturn( [
44 'tables' => [ 'actormigration' => 'table' ],
45 'fields' => [
46 'rc_user' => 'actormigration_user',
47 'rc_user_text' => 'actormigration_user_text',
48 'rc_actor' => 'actormigration_actor',
49 ],
50 'joins' => [ 'actormigration' => 'join' ],
51 ] );
52 $mockStore->expects( $this->any() )
53 ->method( 'getWhere' )
54 ->willReturn( [
55 'tables' => [ 'actormigration' => 'table' ],
56 'conds' => 'actormigration_conds',
57 'joins' => [ 'actormigration' => 'join' ],
58 ] );
59 $mockStore->expects( $this->any() )
60 ->method( 'isAnon' )
61 ->willReturn( 'actormigration is anon' );
62 $mockStore->expects( $this->any() )
63 ->method( 'isNotAnon' )
64 ->willReturn( 'actormigration is not anon' );
65 return $mockStore;
66 }
67
68 /**
69 * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
70 * @param PHPUnit_Framework_MockObject_MockObject|PermissionManager $mockPM
71 * @return WatchedItemQueryService
72 */
73 private function newService( $mockDb, $mockPM = null ) {
74 return new WatchedItemQueryService(
75 $this->getMockLoadBalancer( $mockDb ),
76 $this->getMockCommentStore(),
77 $this->getMockActorMigration(),
78 $this->getMockWatchedItemStore(),
79 $mockPM ?: $this->getMockPermissionManager()
80 );
81 }
82
83 /**
84 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
85 */
86 private function getMockDb() {
87 $mock = $this->createMock( IDatabase::class );
88
89 $mock->expects( $this->any() )
90 ->method( 'makeList' )
91 ->with(
92 $this->isType( 'array' ),
93 $this->isType( 'int' )
94 )
95 ->will( $this->returnCallback( function ( $a, $conj ) {
96 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
97 $conds = [];
98 foreach ( $a as $k => $v ) {
99 if ( is_int( $k ) ) {
100 $conds[] = "($v)";
101 } elseif ( is_array( $v ) ) {
102 $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
103 } else {
104 $conds[] = "($k = '$v')";
105 }
106 }
107 return implode( $sqlConj, $conds );
108 } ) );
109
110 $mock->expects( $this->any() )
111 ->method( 'addQuotes' )
112 ->will( $this->returnCallback( function ( $value ) {
113 return "'$value'";
114 } ) );
115
116 $mock->expects( $this->any() )
117 ->method( 'timestamp' )
118 ->will( $this->returnArgument( 0 ) );
119
120 $mock->expects( $this->any() )
121 ->method( 'bitAnd' )
122 ->willReturnCallback( function ( $a, $b ) {
123 return "($a & $b)";
124 } );
125
126 return $mock;
127 }
128
129 /**
130 * @param PHPUnit_Framework_MockObject_MockObject|IDatabase $mockDb
131 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
132 */
133 private function getMockLoadBalancer( $mockDb ) {
134 $mock = $this->getMockBuilder( LoadBalancer::class )
135 ->disableOriginalConstructor()
136 ->getMock();
137 $mock->expects( $this->any() )
138 ->method( 'getConnectionRef' )
139 ->with( DB_REPLICA )
140 ->will( $this->returnValue( $mockDb ) );
141 return $mock;
142 }
143
144 /**
145 * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
146 */
147 private function getMockWatchedItemStore() {
148 $mock = $this->getMockBuilder( WatchedItemStore::class )
149 ->disableOriginalConstructor()
150 ->getMock();
151 $mock->expects( $this->any() )
152 ->method( 'getLatestNotificationTimestamp' )
153 ->will( $this->returnCallback( function ( $timestamp ) {
154 return $timestamp;
155 } ) );
156 return $mock;
157 }
158
159 /**
160 * @param string $notAllowedAction
161 * @return PHPUnit_Framework_MockObject_MockObject|PermissionManager
162 */
163 private function getMockPermissionManager( $notAllowedAction = null ) {
164 $mock = $this->getMockBuilder( PermissionManager::class )
165 ->disableOriginalConstructor()
166 ->getMock();
167 $mock->method( 'userHasRight' )
168 ->will( $this->returnCallback( function ( $user, $action ) use ( $notAllowedAction ) {
169 return $action !== $notAllowedAction;
170 } ) );
171 $mock->method( 'userHasAnyRight' )
172 ->will( $this->returnCallback( function ( $user, ...$actions ) use ( $notAllowedAction ) {
173 return !in_array( $notAllowedAction, $actions );
174 } ) );
175 return $mock;
176 }
177
178 /**
179 * @param int $id
180 * @param string[] $extraMethods Extra methods that are expected might be called
181 * @return PHPUnit_Framework_MockObject_MockObject|User
182 */
183 private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
184 $mock = $this->getMockBuilder( User::class )->getMock();
185 $mock->method( 'isRegistered' )->willReturn( true );
186 $mock->method( 'getId' )->willReturn( $id );
187 $methods = array_merge( [
188 'isRegistered',
189 'getId',
190 ], $extraMethods );
191 $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) );
192 return $mock;
193 }
194
195 /**
196 * @param int $id
197 * @param string[] $extraMethods Extra methods that are expected might be called
198 * @return PHPUnit_Framework_MockObject_MockObject|User
199 */
200 private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
201 $mock = $this->getMockNonAnonUserWithId( $id,
202 array_merge( [ 'useRCPatrol' ], $extraMethods ) );
203 $mock->method( 'useRCPatrol' )->willReturn( true );
204 return $mock;
205 }
206
207 /**
208 * @param int $id
209 * @param string $notAllowedAction
210 * @return PHPUnit_Framework_MockObject_MockObject|User
211 */
212 private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id ) {
213 $mock = $this->getMockNonAnonUserWithId( $id,
214 [ 'useRCPatrol', 'useNPPatrol' ] );
215 $mock->method( 'useRCPatrol' )->willReturn( false );
216 $mock->method( 'useNPPatrol' )->willReturn( false );
217
218 return $mock;
219 }
220
221 /**
222 * @param int $id
223 * @return PHPUnit_Framework_MockObject_MockObject|User
224 */
225 private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
226 $mock = $this->getMockNonAnonUserWithId( $id,
227 [ 'useRCPatrol', 'useNPPatrol' ] );
228 $mock->expects( $this->any() )
229 ->method( 'useRCPatrol' )
230 ->will( $this->returnValue( false ) );
231 $mock->expects( $this->any() )
232 ->method( 'useNPPatrol' )
233 ->will( $this->returnValue( false ) );
234
235 return $mock;
236 }
237
238 private function getFakeRow( array $rowValues ) {
239 $fakeRow = new stdClass();
240 foreach ( $rowValues as $valueName => $value ) {
241 $fakeRow->$valueName = $value;
242 }
243 return $fakeRow;
244 }
245
246 public function testGetWatchedItemsWithRecentChangeInfo() {
247 $mockDb = $this->getMockDb();
248 $mockDb->expects( $this->once() )
249 ->method( 'select' )
250 ->with(
251 [ 'recentchanges', 'watchlist', 'page' ],
252 [
253 'rc_id',
254 'rc_namespace',
255 'rc_title',
256 'rc_timestamp',
257 'rc_type',
258 'rc_deleted',
259 'wl_notificationtimestamp',
260 'rc_cur_id',
261 'rc_this_oldid',
262 'rc_last_oldid',
263 ],
264 [
265 'wl_user' => 1,
266 '(rc_this_oldid=page_latest) OR (rc_type=3)',
267 ],
268 $this->isType( 'string' ),
269 [
270 'LIMIT' => 3,
271 ],
272 [
273 'watchlist' => [
274 'JOIN',
275 [
276 'wl_namespace=rc_namespace',
277 'wl_title=rc_title'
278 ]
279 ],
280 'page' => [
281 'LEFT JOIN',
282 'rc_cur_id=page_id',
283 ],
284 ]
285 )
286 ->will( $this->returnValue( [
287 $this->getFakeRow( [
288 'rc_id' => 1,
289 'rc_namespace' => 0,
290 'rc_title' => 'Foo1',
291 'rc_timestamp' => '20151212010101',
292 'rc_type' => RC_NEW,
293 'rc_deleted' => 0,
294 'wl_notificationtimestamp' => '20151212010101',
295 ] ),
296 $this->getFakeRow( [
297 'rc_id' => 2,
298 'rc_namespace' => 1,
299 'rc_title' => 'Foo2',
300 'rc_timestamp' => '20151212010102',
301 'rc_type' => RC_NEW,
302 'rc_deleted' => 0,
303 'wl_notificationtimestamp' => null,
304 ] ),
305 $this->getFakeRow( [
306 'rc_id' => 3,
307 'rc_namespace' => 1,
308 'rc_title' => 'Foo3',
309 'rc_timestamp' => '20151212010103',
310 'rc_type' => RC_NEW,
311 'rc_deleted' => 0,
312 'wl_notificationtimestamp' => null,
313 ] ),
314 ] ) );
315
316 $queryService = $this->newService( $mockDb );
317 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
318
319 $startFrom = null;
320 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
321 $user, [ 'limit' => 2 ], $startFrom
322 );
323
324 $this->assertInternalType( 'array', $items );
325 $this->assertCount( 2, $items );
326
327 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
328 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
329 $this->assertInternalType( 'array', $recentChangeInfo );
330 }
331
332 $this->assertEquals(
333 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
334 $items[0][0]
335 );
336 $this->assertEquals(
337 [
338 'rc_id' => 1,
339 'rc_namespace' => 0,
340 'rc_title' => 'Foo1',
341 'rc_timestamp' => '20151212010101',
342 'rc_type' => RC_NEW,
343 'rc_deleted' => 0,
344 ],
345 $items[0][1]
346 );
347
348 $this->assertEquals(
349 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
350 $items[1][0]
351 );
352 $this->assertEquals(
353 [
354 'rc_id' => 2,
355 'rc_namespace' => 1,
356 'rc_title' => 'Foo2',
357 'rc_timestamp' => '20151212010102',
358 'rc_type' => RC_NEW,
359 'rc_deleted' => 0,
360 ],
361 $items[1][1]
362 );
363
364 $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
365 }
366
367 public function testGetWatchedItemsWithRecentChangeInfo_extension() {
368 $mockDb = $this->getMockDb();
369 $mockDb->expects( $this->once() )
370 ->method( 'select' )
371 ->with(
372 [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
373 [
374 'rc_id',
375 'rc_namespace',
376 'rc_title',
377 'rc_timestamp',
378 'rc_type',
379 'rc_deleted',
380 'wl_notificationtimestamp',
381 'rc_cur_id',
382 'rc_this_oldid',
383 'rc_last_oldid',
384 'extension_dummy_field',
385 ],
386 [
387 'wl_user' => 1,
388 '(rc_this_oldid=page_latest) OR (rc_type=3)',
389 'extension_dummy_cond',
390 ],
391 $this->isType( 'string' ),
392 [
393 'extension_dummy_option',
394 ],
395 [
396 'watchlist' => [
397 'JOIN',
398 [
399 'wl_namespace=rc_namespace',
400 'wl_title=rc_title'
401 ]
402 ],
403 'page' => [
404 'LEFT JOIN',
405 'rc_cur_id=page_id',
406 ],
407 'extension_dummy_join_cond' => [],
408 ]
409 )
410 ->will( $this->returnValue( [
411 $this->getFakeRow( [
412 'rc_id' => 1,
413 'rc_namespace' => 0,
414 'rc_title' => 'Foo1',
415 'rc_timestamp' => '20151212010101',
416 'rc_type' => RC_NEW,
417 'rc_deleted' => 0,
418 'wl_notificationtimestamp' => '20151212010101',
419 ] ),
420 $this->getFakeRow( [
421 'rc_id' => 2,
422 'rc_namespace' => 1,
423 'rc_title' => 'Foo2',
424 'rc_timestamp' => '20151212010102',
425 'rc_type' => RC_NEW,
426 'rc_deleted' => 0,
427 'wl_notificationtimestamp' => null,
428 ] ),
429 ] ) );
430
431 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
432
433 $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class )
434 ->getMock();
435 $mockExtension->expects( $this->once() )
436 ->method( 'modifyWatchedItemsWithRCInfoQuery' )
437 ->with(
438 $this->identicalTo( $user ),
439 $this->isType( 'array' ),
440 $this->isInstanceOf( IDatabase::class ),
441 $this->isType( 'array' ),
442 $this->isType( 'array' ),
443 $this->isType( 'array' ),
444 $this->isType( 'array' ),
445 $this->isType( 'array' )
446 )
447 ->will( $this->returnCallback( function (
448 $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
449 ) {
450 $tables[] = 'extension_dummy_table';
451 $fields[] = 'extension_dummy_field';
452 $conds[] = 'extension_dummy_cond';
453 $dbOptions[] = 'extension_dummy_option';
454 $joinConds['extension_dummy_join_cond'] = [];
455 } ) );
456 $mockExtension->expects( $this->once() )
457 ->method( 'modifyWatchedItemsWithRCInfo' )
458 ->with(
459 $this->identicalTo( $user ),
460 $this->isType( 'array' ),
461 $this->isInstanceOf( IDatabase::class ),
462 $this->isType( 'array' ),
463 $this->anything(),
464 $this->anything() // Can't test for null here, PHPUnit applies this after the callback
465 )
466 ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
467 foreach ( $items as $i => &$item ) {
468 $item[1]['extension_dummy_field'] = $i;
469 }
470 unset( $item );
471
472 $this->assertNull( $startFrom );
473 $startFrom = [ '20160203123456', 42 ];
474 } ) );
475
476 $queryService = $this->newService( $mockDb );
477 TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ];
478
479 $startFrom = null;
480 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
481 $user, [], $startFrom
482 );
483
484 $this->assertInternalType( 'array', $items );
485 $this->assertCount( 2, $items );
486
487 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
488 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
489 $this->assertInternalType( 'array', $recentChangeInfo );
490 }
491
492 $this->assertEquals(
493 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
494 $items[0][0]
495 );
496 $this->assertEquals(
497 [
498 'rc_id' => 1,
499 'rc_namespace' => 0,
500 'rc_title' => 'Foo1',
501 'rc_timestamp' => '20151212010101',
502 'rc_type' => RC_NEW,
503 'rc_deleted' => 0,
504 'extension_dummy_field' => 0,
505 ],
506 $items[0][1]
507 );
508
509 $this->assertEquals(
510 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
511 $items[1][0]
512 );
513 $this->assertEquals(
514 [
515 'rc_id' => 2,
516 'rc_namespace' => 1,
517 'rc_title' => 'Foo2',
518 'rc_timestamp' => '20151212010102',
519 'rc_type' => RC_NEW,
520 'rc_deleted' => 0,
521 'extension_dummy_field' => 1,
522 ],
523 $items[1][1]
524 );
525
526 $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
527 }
528
529 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
530 return [
531 [
532 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
533 null,
534 [],
535 [ 'rc_type', 'rc_minor', 'rc_bot' ],
536 [],
537 [],
538 [],
539 ],
540 [
541 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
542 null,
543 [ 'actormigration' => 'table' ],
544 [ 'rc_user_text' => 'actormigration_user_text' ],
545 [],
546 [],
547 [ 'actormigration' => 'join' ],
548 ],
549 [
550 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
551 null,
552 [ 'actormigration' => 'table' ],
553 [ 'rc_user' => 'actormigration_user' ],
554 [],
555 [],
556 [ 'actormigration' => 'join' ],
557 ],
558 [
559 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
560 null,
561 [ 'commentstore' => 'table' ],
562 [ 'commentstore' => 'field' ],
563 [],
564 [],
565 [ 'commentstore' => 'join' ],
566 ],
567 [
568 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
569 null,
570 [],
571 [ 'rc_patrolled', 'rc_log_type' ],
572 [],
573 [],
574 [],
575 ],
576 [
577 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
578 null,
579 [],
580 [ 'rc_old_len', 'rc_new_len' ],
581 [],
582 [],
583 [],
584 ],
585 [
586 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
587 null,
588 [],
589 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
590 [],
591 [],
592 [],
593 ],
594 [
595 [ 'namespaceIds' => [ 0, 1 ] ],
596 null,
597 [],
598 [],
599 [ 'wl_namespace' => [ 0, 1 ] ],
600 [],
601 [],
602 ],
603 [
604 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
605 null,
606 [],
607 [],
608 [ 'wl_namespace' => [ 0, 1 ] ],
609 [],
610 [],
611 ],
612 [
613 [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
614 null,
615 [],
616 [],
617 [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
618 [],
619 [],
620 ],
621 [
622 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
623 null,
624 [],
625 [],
626 [],
627 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
628 [],
629 ],
630 [
631 [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
632 null,
633 [],
634 [],
635 [],
636 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
637 [],
638 ],
639 [
640 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
641 null,
642 [],
643 [],
644 [ "rc_timestamp <= '20151212010101'" ],
645 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
646 [],
647 ],
648 [
649 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
650 null,
651 [],
652 [],
653 [ "rc_timestamp >= '20151212010101'" ],
654 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
655 [],
656 ],
657 [
658 [
659 'dir' => WatchedItemQueryService::DIR_OLDER,
660 'start' => '20151212020101',
661 'end' => '20151212010101'
662 ],
663 null,
664 [],
665 [],
666 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
667 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
668 [],
669 ],
670 [
671 [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
672 null,
673 [],
674 [],
675 [ "rc_timestamp >= '20151212010101'" ],
676 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
677 [],
678 ],
679 [
680 [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
681 null,
682 [],
683 [],
684 [ "rc_timestamp <= '20151212010101'" ],
685 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
686 [],
687 ],
688 [
689 [
690 'dir' => WatchedItemQueryService::DIR_NEWER,
691 'start' => '20151212010101',
692 'end' => '20151212020101'
693 ],
694 null,
695 [],
696 [],
697 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
698 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
699 [],
700 ],
701 [
702 [ 'limit' => 10 ],
703 null,
704 [],
705 [],
706 [],
707 [ 'LIMIT' => 11 ],
708 [],
709 ],
710 [
711 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
712 null,
713 [],
714 [],
715 [],
716 [ 'LIMIT' => 11 ],
717 [],
718 ],
719 [
720 [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
721 null,
722 [],
723 [],
724 [ 'rc_minor != 0' ],
725 [],
726 [],
727 ],
728 [
729 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
730 null,
731 [],
732 [],
733 [ 'rc_minor = 0' ],
734 [],
735 [],
736 ],
737 [
738 [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
739 null,
740 [],
741 [],
742 [ 'rc_bot != 0' ],
743 [],
744 [],
745 ],
746 [
747 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
748 null,
749 [],
750 [],
751 [ 'rc_bot = 0' ],
752 [],
753 [],
754 ],
755 [
756 [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
757 null,
758 [ 'actormigration' => 'table' ],
759 [],
760 [ 'actormigration is anon' ],
761 [],
762 [ 'actormigration' => 'join' ],
763 ],
764 [
765 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
766 null,
767 [ 'actormigration' => 'table' ],
768 [],
769 [ 'actormigration is not anon' ],
770 [],
771 [ 'actormigration' => 'join' ],
772 ],
773 [
774 [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
775 null,
776 [],
777 [],
778 [ 'rc_patrolled != 0' ],
779 [],
780 [],
781 ],
782 [
783 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
784 null,
785 [],
786 [],
787 [ 'rc_patrolled' => 0 ],
788 [],
789 [],
790 ],
791 [
792 [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
793 null,
794 [],
795 [],
796 [ 'rc_timestamp >= wl_notificationtimestamp' ],
797 [],
798 [],
799 ],
800 [
801 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
802 null,
803 [],
804 [],
805 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
806 [],
807 [],
808 ],
809 [
810 [ 'onlyByUser' => 'SomeOtherUser' ],
811 null,
812 [ 'actormigration' => 'table' ],
813 [],
814 [ 'actormigration_conds' ],
815 [],
816 [ 'actormigration' => 'join' ],
817 ],
818 [
819 [ 'notByUser' => 'SomeOtherUser' ],
820 null,
821 [ 'actormigration' => 'table' ],
822 [],
823 [ 'NOT(actormigration_conds)' ],
824 [],
825 [ 'actormigration' => 'join' ],
826 ],
827 [
828 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
829 [ '20151212010101', 123 ],
830 [],
831 [],
832 [
833 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
834 ],
835 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
836 [],
837 ],
838 [
839 [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
840 [ '20151212010101', 123 ],
841 [],
842 [],
843 [
844 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
845 ],
846 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
847 [],
848 ],
849 [
850 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
851 [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
852 [],
853 [],
854 [
855 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
856 ],
857 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
858 [],
859 ],
860 ];
861 }
862
863 /**
864 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
865 */
866 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
867 array $options,
868 $startFrom,
869 array $expectedExtraTables,
870 array $expectedExtraFields,
871 array $expectedExtraConds,
872 array $expectedDbOptions,
873 array $expectedExtraJoinConds
874 ) {
875 $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
876 $expectedFields = array_merge(
877 [
878 'rc_id',
879 'rc_namespace',
880 'rc_title',
881 'rc_timestamp',
882 'rc_type',
883 'rc_deleted',
884 'wl_notificationtimestamp',
885
886 'rc_cur_id',
887 'rc_this_oldid',
888 'rc_last_oldid',
889 ],
890 $expectedExtraFields
891 );
892 $expectedConds = array_merge(
893 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
894 $expectedExtraConds
895 );
896 $expectedJoinConds = array_merge(
897 [
898 'watchlist' => [
899 'JOIN',
900 [
901 'wl_namespace=rc_namespace',
902 'wl_title=rc_title'
903 ]
904 ],
905 'page' => [
906 'LEFT JOIN',
907 'rc_cur_id=page_id',
908 ],
909 ],
910 $expectedExtraJoinConds
911 );
912
913 $mockDb = $this->getMockDb();
914 $mockDb->expects( $this->once() )
915 ->method( 'select' )
916 ->with(
917 $expectedTables,
918 $expectedFields,
919 $expectedConds,
920 $this->isType( 'string' ),
921 $expectedDbOptions,
922 $expectedJoinConds
923 )
924 ->will( $this->returnValue( [] ) );
925
926 $queryService = $this->newService( $mockDb );
927 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
928
929 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
930
931 $this->assertEmpty( $items );
932 $this->assertNull( $startFrom );
933 }
934
935 public function filterPatrolledOptionProvider() {
936 return [
937 [ WatchedItemQueryService::FILTER_PATROLLED ],
938 [ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
939 ];
940 }
941
942 /**
943 * @dataProvider filterPatrolledOptionProvider
944 */
945 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
946 $filtersOption
947 ) {
948 $mockDb = $this->getMockDb();
949 $mockDb->expects( $this->once() )
950 ->method( 'select' )
951 ->with(
952 [ 'recentchanges', 'watchlist', 'page' ],
953 $this->isType( 'array' ),
954 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
955 $this->isType( 'string' ),
956 $this->isType( 'array' ),
957 $this->isType( 'array' )
958 )
959 ->will( $this->returnValue( [] ) );
960
961 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
962
963 $queryService = $this->newService( $mockDb );
964 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
965 $user,
966 [ 'filters' => [ $filtersOption ] ]
967 );
968
969 $this->assertEmpty( $items );
970 }
971
972 public function mysqlIndexOptimizationProvider() {
973 return [
974 [
975 'mysql',
976 [],
977 [ "rc_timestamp > ''" ],
978 ],
979 [
980 'mysql',
981 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
982 [ "rc_timestamp <= '20151212010101'" ],
983 ],
984 [
985 'mysql',
986 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
987 [ "rc_timestamp >= '20151212010101'" ],
988 ],
989 [
990 'postgres',
991 [],
992 [],
993 ],
994 ];
995 }
996
997 /**
998 * @dataProvider mysqlIndexOptimizationProvider
999 */
1000 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
1001 $dbType,
1002 array $options,
1003 array $expectedExtraConds
1004 ) {
1005 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1006 $conds = array_merge( $commonConds, $expectedExtraConds );
1007
1008 $mockDb = $this->getMockDb();
1009 $mockDb->expects( $this->once() )
1010 ->method( 'select' )
1011 ->with(
1012 [ 'recentchanges', 'watchlist', 'page' ],
1013 $this->isType( 'array' ),
1014 $conds,
1015 $this->isType( 'string' ),
1016 $this->isType( 'array' ),
1017 $this->isType( 'array' )
1018 )
1019 ->will( $this->returnValue( [] ) );
1020 $mockDb->expects( $this->any() )
1021 ->method( 'getType' )
1022 ->will( $this->returnValue( $dbType ) );
1023
1024 $queryService = $this->newService( $mockDb );
1025 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1026
1027 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1028
1029 $this->assertEmpty( $items );
1030 }
1031
1032 public function userPermissionRelatedExtraChecksProvider() {
1033 return [
1034 [
1035 [],
1036 'deletedhistory',
1037 [],
1038 [
1039 '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1040 LogPage::DELETED_ACTION . ')'
1041 ],
1042 [],
1043 ],
1044 [
1045 [],
1046 'suppressrevision',
1047 [],
1048 [
1049 '(rc_type != ' . RC_LOG . ') OR (' .
1050 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1051 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
1052 ],
1053 [],
1054 ],
1055 [
1056 [],
1057 'viewsuppressed',
1058 [],
1059 [
1060 '(rc_type != ' . RC_LOG . ') OR (' .
1061 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1062 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
1063 ],
1064 [],
1065 ],
1066 [
1067 [ 'onlyByUser' => 'SomeOtherUser' ],
1068 'deletedhistory',
1069 [ 'actormigration' => 'table' ],
1070 [
1071 'actormigration_conds',
1072 '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
1073 '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1074 LogPage::DELETED_ACTION . ')'
1075 ],
1076 [ 'actormigration' => 'join' ],
1077 ],
1078 [
1079 [ 'onlyByUser' => 'SomeOtherUser' ],
1080 'suppressrevision',
1081 [ 'actormigration' => 'table' ],
1082 [
1083 'actormigration_conds',
1084 '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1085 ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
1086 '(rc_type != ' . RC_LOG . ') OR (' .
1087 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1088 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
1089 ],
1090 [ 'actormigration' => 'join' ],
1091 ],
1092 [
1093 [ 'onlyByUser' => 'SomeOtherUser' ],
1094 'viewsuppressed',
1095 [ 'actormigration' => 'table' ],
1096 [
1097 'actormigration_conds',
1098 '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1099 ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
1100 '(rc_type != ' . RC_LOG . ') OR (' .
1101 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1102 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
1103 ],
1104 [ 'actormigration' => 'join' ],
1105 ],
1106 ];
1107 }
1108
1109 /**
1110 * @dataProvider userPermissionRelatedExtraChecksProvider
1111 */
1112 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
1113 array $options,
1114 $notAllowedAction,
1115 array $expectedExtraTables,
1116 array $expectedExtraConds,
1117 array $expectedExtraJoins
1118 ) {
1119 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1120 $conds = array_merge( $commonConds, $expectedExtraConds );
1121
1122 $mockDb = $this->getMockDb();
1123 $mockDb->expects( $this->once() )
1124 ->method( 'select' )
1125 ->with(
1126 array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1127 $this->isType( 'array' ),
1128 $conds,
1129 $this->isType( 'string' ),
1130 $this->isType( 'array' ),
1131 array_merge( [
1132 'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1133 'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1134 ], $expectedExtraJoins )
1135 )
1136 ->will( $this->returnValue( [] ) );
1137
1138 $permissionManager = $this->getMockPermissionManager( $notAllowedAction );
1139 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1 );
1140
1141 $queryService = $this->newService( $mockDb, $permissionManager );
1142 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1143
1144 $this->assertEmpty( $items );
1145 }
1146
1147 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
1148 $mockDb = $this->getMockDb();
1149 $mockDb->expects( $this->once() )
1150 ->method( 'select' )
1151 ->with(
1152 [ 'recentchanges', 'watchlist' ],
1153 [
1154 'rc_id',
1155 'rc_namespace',
1156 'rc_title',
1157 'rc_timestamp',
1158 'rc_type',
1159 'rc_deleted',
1160 'wl_notificationtimestamp',
1161
1162 'rc_cur_id',
1163 'rc_this_oldid',
1164 'rc_last_oldid',
1165 ],
1166 [ 'wl_user' => 1, ],
1167 $this->isType( 'string' ),
1168 [],
1169 [
1170 'watchlist' => [
1171 'JOIN',
1172 [
1173 'wl_namespace=rc_namespace',
1174 'wl_title=rc_title'
1175 ]
1176 ],
1177 ]
1178 )
1179 ->will( $this->returnValue( [] ) );
1180
1181 $queryService = $this->newService( $mockDb );
1182 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1183
1184 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1185
1186 $this->assertEmpty( $items );
1187 }
1188
1189 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
1190 return [
1191 [
1192 [ 'rcTypes' => [ 1337 ] ],
1193 null,
1194 'Bad value for parameter $options[\'rcTypes\']',
1195 ],
1196 [
1197 [ 'rcTypes' => [ 'edit' ] ],
1198 null,
1199 'Bad value for parameter $options[\'rcTypes\']',
1200 ],
1201 [
1202 [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
1203 null,
1204 'Bad value for parameter $options[\'rcTypes\']',
1205 ],
1206 [
1207 [ 'dir' => 'foo' ],
1208 null,
1209 'Bad value for parameter $options[\'dir\']',
1210 ],
1211 [
1212 [ 'start' => '20151212010101' ],
1213 null,
1214 'Bad value for parameter $options[\'dir\']: must be provided',
1215 ],
1216 [
1217 [ 'end' => '20151212010101' ],
1218 null,
1219 'Bad value for parameter $options[\'dir\']: must be provided',
1220 ],
1221 [
1222 [],
1223 [ '20151212010101', 123 ],
1224 'Bad value for parameter $options[\'dir\']: must be provided',
1225 ],
1226 [
1227 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
1228 '20151212010101',
1229 'Bad value for parameter $startFrom: must be a two-element array',
1230 ],
1231 [
1232 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
1233 [ '20151212010101' ],
1234 'Bad value for parameter $startFrom: must be a two-element array',
1235 ],
1236 [
1237 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
1238 [ '20151212010101', 123, 'foo' ],
1239 'Bad value for parameter $startFrom: must be a two-element array',
1240 ],
1241 [
1242 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1243 null,
1244 'Bad value for parameter $options[\'watchlistOwnerToken\']',
1245 ],
1246 [
1247 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1248 null,
1249 'Bad value for parameter $options[\'watchlistOwner\']',
1250 ],
1251 ];
1252 }
1253
1254 /**
1255 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
1256 */
1257 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
1258 array $options,
1259 $startFrom,
1260 $expectedInExceptionMessage
1261 ) {
1262 $mockDb = $this->getMockDb();
1263 $mockDb->expects( $this->never() )
1264 ->method( $this->anything() );
1265
1266 $queryService = $this->newService( $mockDb );
1267 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1268
1269 $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1270 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1271 }
1272
1273 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
1274 $mockDb = $this->getMockDb();
1275 $mockDb->expects( $this->once() )
1276 ->method( 'select' )
1277 ->with(
1278 [ 'recentchanges', 'watchlist', 'page' ],
1279 [
1280 'rc_id',
1281 'rc_namespace',
1282 'rc_title',
1283 'rc_timestamp',
1284 'rc_type',
1285 'rc_deleted',
1286 'wl_notificationtimestamp',
1287 'rc_cur_id',
1288 ],
1289 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1290 $this->isType( 'string' ),
1291 [],
1292 [
1293 'watchlist' => [
1294 'JOIN',
1295 [
1296 'wl_namespace=rc_namespace',
1297 'wl_title=rc_title'
1298 ]
1299 ],
1300 'page' => [
1301 'LEFT JOIN',
1302 'rc_cur_id=page_id',
1303 ],
1304 ]
1305 )
1306 ->will( $this->returnValue( [] ) );
1307
1308 $queryService = $this->newService( $mockDb );
1309 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1310
1311 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1312 $user,
1313 [ 'usedInGenerator' => true ]
1314 );
1315
1316 $this->assertEmpty( $items );
1317 }
1318
1319 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
1320 $mockDb = $this->getMockDb();
1321 $mockDb->expects( $this->once() )
1322 ->method( 'select' )
1323 ->with(
1324 [ 'recentchanges', 'watchlist' ],
1325 [
1326 'rc_id',
1327 'rc_namespace',
1328 'rc_title',
1329 'rc_timestamp',
1330 'rc_type',
1331 'rc_deleted',
1332 'wl_notificationtimestamp',
1333 'rc_this_oldid',
1334 ],
1335 [ 'wl_user' => 1 ],
1336 $this->isType( 'string' ),
1337 [],
1338 [
1339 'watchlist' => [
1340 'JOIN',
1341 [
1342 'wl_namespace=rc_namespace',
1343 'wl_title=rc_title'
1344 ]
1345 ],
1346 ]
1347 )
1348 ->will( $this->returnValue( [] ) );
1349
1350 $queryService = $this->newService( $mockDb );
1351 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1352
1353 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1354 $user,
1355 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1356 );
1357
1358 $this->assertEmpty( $items );
1359 }
1360
1361 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
1362 $mockDb = $this->getMockDb();
1363 $mockDb->expects( $this->once() )
1364 ->method( 'select' )
1365 ->with(
1366 $this->isType( 'array' ),
1367 $this->isType( 'array' ),
1368 [
1369 'wl_user' => 2,
1370 '(rc_this_oldid=page_latest) OR (rc_type=3)',
1371 ],
1372 $this->isType( 'string' ),
1373 $this->isType( 'array' ),
1374 $this->isType( 'array' )
1375 )
1376 ->will( $this->returnValue( [] ) );
1377
1378 $queryService = $this->newService( $mockDb );
1379 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1380 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1381 $otherUser->expects( $this->once() )
1382 ->method( 'getOption' )
1383 ->with( 'watchlisttoken' )
1384 ->willReturn( '0123456789abcdef' );
1385
1386 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1387 $user,
1388 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1389 );
1390
1391 $this->assertEmpty( $items );
1392 }
1393
1394 public function invalidWatchlistTokenProvider() {
1395 return [
1396 [ 'wrongToken' ],
1397 [ '' ],
1398 ];
1399 }
1400
1401 /**
1402 * @dataProvider invalidWatchlistTokenProvider
1403 */
1404 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1405 $mockDb = $this->getMockDb();
1406 $mockDb->expects( $this->never() )
1407 ->method( $this->anything() );
1408
1409 $queryService = $this->newService( $mockDb );
1410 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1411 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
1412 $otherUser->expects( $this->once() )
1413 ->method( 'getOption' )
1414 ->with( 'watchlisttoken' )
1415 ->willReturn( '0123456789abcdef' );
1416
1417 $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
1418 $queryService->getWatchedItemsWithRecentChangeInfo(
1419 $user,
1420 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1421 );
1422 }
1423
1424 public function testGetWatchedItemsForUser() {
1425 $mockDb = $this->getMockDb();
1426 $mockDb->expects( $this->once() )
1427 ->method( 'select' )
1428 ->with(
1429 'watchlist',
1430 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1431 [ 'wl_user' => 1 ]
1432 )
1433 ->will( $this->returnValue( [
1434 $this->getFakeRow( [
1435 'wl_namespace' => 0,
1436 'wl_title' => 'Foo1',
1437 'wl_notificationtimestamp' => '20151212010101',
1438 ] ),
1439 $this->getFakeRow( [
1440 'wl_namespace' => 1,
1441 'wl_title' => 'Foo2',
1442 'wl_notificationtimestamp' => null,
1443 ] ),
1444 ] ) );
1445
1446 $queryService = $this->newService( $mockDb );
1447 $user = $this->getMockNonAnonUserWithId( 1 );
1448
1449 $items = $queryService->getWatchedItemsForUser( $user );
1450
1451 $this->assertInternalType( 'array', $items );
1452 $this->assertCount( 2, $items );
1453 $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
1454 $this->assertEquals(
1455 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1456 $items[0]
1457 );
1458 $this->assertEquals(
1459 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1460 $items[1]
1461 );
1462 }
1463
1464 public function provideGetWatchedItemsForUserOptions() {
1465 return [
1466 [
1467 [ 'namespaceIds' => [ 0, 1 ], ],
1468 [ 'wl_namespace' => [ 0, 1 ], ],
1469 []
1470 ],
1471 [
1472 [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
1473 [],
1474 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1475 ],
1476 [
1477 [
1478 'namespaceIds' => [ 0 ],
1479 'sort' => WatchedItemQueryService::SORT_ASC,
1480 ],
1481 [ 'wl_namespace' => [ 0 ], ],
1482 [ 'ORDER BY' => 'wl_title ASC' ]
1483 ],
1484 [
1485 [ 'limit' => 10 ],
1486 [],
1487 [ 'LIMIT' => 10 ]
1488 ],
1489 [
1490 [
1491 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1492 'limit' => "10; DROP TABLE watchlist;\n--",
1493 ],
1494 [ 'wl_namespace' => [ 0, 1 ], ],
1495 [ 'LIMIT' => 10 ]
1496 ],
1497 [
1498 [ 'filter' => WatchedItemQueryService::FILTER_CHANGED ],
1499 [ 'wl_notificationtimestamp IS NOT NULL' ],
1500 []
1501 ],
1502 [
1503 [ 'filter' => WatchedItemQueryService::FILTER_NOT_CHANGED ],
1504 [ 'wl_notificationtimestamp IS NULL' ],
1505 []
1506 ],
1507 [
1508 [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
1509 [],
1510 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1511 ],
1512 [
1513 [
1514 'namespaceIds' => [ 0 ],
1515 'sort' => WatchedItemQueryService::SORT_DESC,
1516 ],
1517 [ 'wl_namespace' => [ 0 ], ],
1518 [ 'ORDER BY' => 'wl_title DESC' ]
1519 ],
1520 ];
1521 }
1522
1523 /**
1524 * @dataProvider provideGetWatchedItemsForUserOptions
1525 */
1526 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1527 array $options,
1528 array $expectedConds,
1529 array $expectedDbOptions
1530 ) {
1531 $mockDb = $this->getMockDb();
1532 $user = $this->getMockNonAnonUserWithId( 1 );
1533
1534 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1535 $mockDb->expects( $this->once() )
1536 ->method( 'select' )
1537 ->with(
1538 'watchlist',
1539 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1540 $expectedConds,
1541 $this->isType( 'string' ),
1542 $expectedDbOptions
1543 )
1544 ->will( $this->returnValue( [] ) );
1545
1546 $queryService = $this->newService( $mockDb );
1547
1548 $items = $queryService->getWatchedItemsForUser( $user, $options );
1549 $this->assertEmpty( $items );
1550 }
1551
1552 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1553 return [
1554 [
1555 [
1556 'from' => new TitleValue( 0, 'SomeDbKey' ),
1557 'sort' => WatchedItemQueryService::SORT_ASC
1558 ],
1559 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1560 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1561 ],
1562 [
1563 [
1564 'from' => new TitleValue( 0, 'SomeDbKey' ),
1565 'sort' => WatchedItemQueryService::SORT_DESC,
1566 ],
1567 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1568 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1569 ],
1570 [
1571 [
1572 'until' => new TitleValue( 0, 'SomeDbKey' ),
1573 'sort' => WatchedItemQueryService::SORT_ASC
1574 ],
1575 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1576 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1577 ],
1578 [
1579 [
1580 'until' => new TitleValue( 0, 'SomeDbKey' ),
1581 'sort' => WatchedItemQueryService::SORT_DESC
1582 ],
1583 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1584 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1585 ],
1586 [
1587 [
1588 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1589 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1590 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1591 'sort' => WatchedItemQueryService::SORT_ASC
1592 ],
1593 [
1594 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1595 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1596 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1597 ],
1598 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1599 ],
1600 [
1601 [
1602 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1603 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1604 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1605 'sort' => WatchedItemQueryService::SORT_DESC
1606 ],
1607 [
1608 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1609 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1610 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1611 ],
1612 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1613 ],
1614 ];
1615 }
1616
1617 /**
1618 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1619 */
1620 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1621 array $options,
1622 array $expectedConds,
1623 array $expectedDbOptions
1624 ) {
1625 $user = $this->getMockNonAnonUserWithId( 1 );
1626
1627 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1628
1629 $mockDb = $this->getMockDb();
1630 $mockDb->expects( $this->any() )
1631 ->method( 'addQuotes' )
1632 ->will( $this->returnCallback( function ( $value ) {
1633 return "'$value'";
1634 } ) );
1635 $mockDb->expects( $this->any() )
1636 ->method( 'makeList' )
1637 ->with(
1638 $this->isType( 'array' ),
1639 $this->isType( 'int' )
1640 )
1641 ->will( $this->returnCallback( function ( $a, $conj ) {
1642 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
1643 return implode( $sqlConj, array_map( function ( $s ) {
1644 return '(' . $s . ')';
1645 }, $a
1646 ) );
1647 } ) );
1648 $mockDb->expects( $this->once() )
1649 ->method( 'select' )
1650 ->with(
1651 'watchlist',
1652 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1653 $expectedConds,
1654 $this->isType( 'string' ),
1655 $expectedDbOptions
1656 )
1657 ->will( $this->returnValue( [] ) );
1658
1659 $queryService = $this->newService( $mockDb );
1660
1661 $items = $queryService->getWatchedItemsForUser( $user, $options );
1662 $this->assertEmpty( $items );
1663 }
1664
1665 public function getWatchedItemsForUserInvalidOptionsProvider() {
1666 return [
1667 [
1668 [ 'sort' => 'foo' ],
1669 'Bad value for parameter $options[\'sort\']'
1670 ],
1671 [
1672 [ 'filter' => 'foo' ],
1673 'Bad value for parameter $options[\'filter\']'
1674 ],
1675 [
1676 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1677 'Bad value for parameter $options[\'sort\']: must be provided'
1678 ],
1679 [
1680 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1681 'Bad value for parameter $options[\'sort\']: must be provided'
1682 ],
1683 [
1684 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1685 'Bad value for parameter $options[\'sort\']: must be provided'
1686 ],
1687 ];
1688 }
1689
1690 /**
1691 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1692 */
1693 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1694 array $options,
1695 $expectedInExceptionMessage
1696 ) {
1697 $queryService = $this->newService( $this->getMockDb() );
1698
1699 $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1700 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1701 }
1702
1703 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1704 $mockDb = $this->getMockDb();
1705
1706 $mockDb->expects( $this->never() )
1707 ->method( $this->anything() );
1708
1709 $queryService = $this->newService( $mockDb );
1710
1711 $items = $queryService->getWatchedItemsForUser(
1712 new UserIdentityValue( 0, 'AnonUser', 0 ) );
1713 $this->assertEmpty( $items );
1714 }
1715
1716 }