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