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