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