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