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