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