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