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