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