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