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