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