Convert WatchedItemQueryService to using getConnectionRef()
[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 [
185 'watchlist' => [
186 'INNER JOIN',
187 [
188 'wl_namespace=rc_namespace',
189 'wl_title=rc_title'
190 ]
191 ],
192 'page' => [
193 'LEFT JOIN',
194 'rc_cur_id=page_id',
195 ],
196 ]
197 )
198 ->will( $this->returnValue( [
199 $this->getFakeRow( [
200 'rc_id' => 1,
201 'rc_namespace' => 0,
202 'rc_title' => 'Foo1',
203 'rc_timestamp' => '20151212010101',
204 'rc_type' => RC_NEW,
205 'rc_deleted' => 0,
206 'wl_notificationtimestamp' => '20151212010101',
207 ] ),
208 $this->getFakeRow( [
209 'rc_id' => 2,
210 'rc_namespace' => 1,
211 'rc_title' => 'Foo2',
212 'rc_timestamp' => '20151212010102',
213 'rc_type' => RC_NEW,
214 'rc_deleted' => 0,
215 'wl_notificationtimestamp' => null,
216 ] ),
217 ] ) );
218
219 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
220 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
221
222 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user );
223
224 $this->assertInternalType( 'array', $items );
225 $this->assertCount( 2, $items );
226
227 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
228 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
229 $this->assertInternalType( 'array', $recentChangeInfo );
230 }
231
232 $this->assertEquals(
233 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
234 $items[0][0]
235 );
236 $this->assertEquals(
237 [
238 'rc_id' => 1,
239 'rc_namespace' => 0,
240 'rc_title' => 'Foo1',
241 'rc_timestamp' => '20151212010101',
242 'rc_type' => RC_NEW,
243 'rc_deleted' => 0,
244 ],
245 $items[0][1]
246 );
247
248 $this->assertEquals(
249 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
250 $items[1][0]
251 );
252 $this->assertEquals(
253 [
254 'rc_id' => 2,
255 'rc_namespace' => 1,
256 'rc_title' => 'Foo2',
257 'rc_timestamp' => '20151212010102',
258 'rc_type' => RC_NEW,
259 'rc_deleted' => 0,
260 ],
261 $items[1][1]
262 );
263 }
264
265 public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
266 return [
267 [
268 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
269 [ 'rc_type', 'rc_minor', 'rc_bot' ],
270 [],
271 [],
272 ],
273 [
274 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
275 [ 'rc_user_text' ],
276 [],
277 [],
278 ],
279 [
280 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
281 [ 'rc_user' ],
282 [],
283 [],
284 ],
285 [
286 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
287 [ 'rc_comment' ],
288 [],
289 [],
290 ],
291 [
292 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
293 [ 'rc_patrolled', 'rc_log_type' ],
294 [],
295 [],
296 ],
297 [
298 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
299 [ 'rc_old_len', 'rc_new_len' ],
300 [],
301 [],
302 ],
303 [
304 [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
305 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
306 [],
307 [],
308 ],
309 [
310 [ 'namespaceIds' => [ 0, 1 ] ],
311 [],
312 [ 'wl_namespace' => [ 0, 1 ] ],
313 [],
314 ],
315 [
316 [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
317 [],
318 [ 'wl_namespace' => [ 0, 1 ] ],
319 [],
320 ],
321 [
322 [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
323 [],
324 [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
325 [],
326 ],
327 [
328 [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
329 [],
330 [],
331 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
332 ],
333 [
334 [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
335 [],
336 [],
337 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
338 ],
339 [
340 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
341 [],
342 [ "rc_timestamp <= '20151212010101'" ],
343 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
344 ],
345 [
346 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
347 [],
348 [ "rc_timestamp >= '20151212010101'" ],
349 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
350 ],
351 [
352 [
353 'dir' => WatchedItemQueryService::DIR_OLDER,
354 'start' => '20151212020101',
355 'end' => '20151212010101'
356 ],
357 [],
358 [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
359 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ]
360 ],
361 [
362 [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
363 [],
364 [ "rc_timestamp >= '20151212010101'" ],
365 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
366 ],
367 [
368 [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
369 [],
370 [ "rc_timestamp <= '20151212010101'" ],
371 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
372 ],
373 [
374 [
375 'dir' => WatchedItemQueryService::DIR_NEWER,
376 'start' => '20151212010101',
377 'end' => '20151212020101'
378 ],
379 [],
380 [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
381 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ]
382 ],
383 [
384 [ 'limit' => 10 ],
385 [],
386 [],
387 [ 'LIMIT' => 10 ],
388 ],
389 [
390 [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
391 [],
392 [],
393 [ 'LIMIT' => 10 ],
394 ],
395 [
396 [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
397 [],
398 [ 'rc_minor != 0' ],
399 [],
400 ],
401 [
402 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
403 [],
404 [ 'rc_minor = 0' ],
405 [],
406 ],
407 [
408 [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
409 [],
410 [ 'rc_bot != 0' ],
411 [],
412 ],
413 [
414 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
415 [],
416 [ 'rc_bot = 0' ],
417 [],
418 ],
419 [
420 [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
421 [],
422 [ 'rc_user = 0' ],
423 [],
424 ],
425 [
426 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
427 [],
428 [ 'rc_user != 0' ],
429 [],
430 ],
431 [
432 [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
433 [],
434 [ 'rc_patrolled != 0' ],
435 [],
436 ],
437 [
438 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
439 [],
440 [ 'rc_patrolled = 0' ],
441 [],
442 ],
443 [
444 [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
445 [],
446 [ 'rc_timestamp >= wl_notificationtimestamp' ],
447 [],
448 ],
449 [
450 [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
451 [],
452 [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
453 [],
454 ],
455 [
456 [ 'onlyByUser' => 'SomeOtherUser' ],
457 [],
458 [ 'rc_user_text' => 'SomeOtherUser' ],
459 [],
460 ],
461 [
462 [ 'notByUser' => 'SomeOtherUser' ],
463 [],
464 [ "rc_user_text != 'SomeOtherUser'" ],
465 [],
466 ],
467 [
468 [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_OLDER ],
469 [],
470 [
471 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
472 ],
473 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
474 ],
475 [
476 [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_NEWER ],
477 [],
478 [
479 "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
480 ],
481 [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
482 ],
483 [
484 [
485 'startFrom' => [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
486 'dir' => WatchedItemQueryService::DIR_OLDER
487 ],
488 [],
489 [
490 "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
491 ],
492 [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
493 ],
494 ];
495 }
496
497 /**
498 * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
499 */
500 public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
501 array $options,
502 array $expectedExtraFields,
503 array $expectedExtraConds,
504 array $expectedDbOptions
505 ) {
506 $expectedFields = array_merge(
507 [
508 'rc_id',
509 'rc_namespace',
510 'rc_title',
511 'rc_timestamp',
512 'rc_type',
513 'rc_deleted',
514 'wl_notificationtimestamp',
515
516 'rc_cur_id',
517 'rc_this_oldid',
518 'rc_last_oldid',
519 ],
520 $expectedExtraFields
521 );
522 $expectedConds = array_merge(
523 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
524 $expectedExtraConds
525 );
526
527 $mockDb = $this->getMockDb();
528 $mockDb->expects( $this->once() )
529 ->method( 'select' )
530 ->with(
531 [ 'recentchanges', 'watchlist', 'page' ],
532 $expectedFields,
533 $expectedConds,
534 $this->isType( 'string' ),
535 $expectedDbOptions,
536 [
537 'watchlist' => [
538 'INNER JOIN',
539 [
540 'wl_namespace=rc_namespace',
541 'wl_title=rc_title'
542 ]
543 ],
544 'page' => [
545 'LEFT JOIN',
546 'rc_cur_id=page_id',
547 ],
548 ]
549 )
550 ->will( $this->returnValue( [] ) );
551
552 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
553 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
554
555 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
556
557 $this->assertEmpty( $items );
558 }
559
560 public function filterPatrolledOptionProvider() {
561 return [
562 [ WatchedItemQueryService::FILTER_PATROLLED ],
563 [ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
564 ];
565 }
566
567 /**
568 * @dataProvider filterPatrolledOptionProvider
569 */
570 public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
571 $filtersOption
572 ) {
573 $mockDb = $this->getMockDb();
574 $mockDb->expects( $this->once() )
575 ->method( 'select' )
576 ->with(
577 [ 'recentchanges', 'watchlist', 'page' ],
578 $this->isType( 'array' ),
579 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
580 $this->isType( 'string' ),
581 $this->isType( 'array' ),
582 $this->isType( 'array' )
583 )
584 ->will( $this->returnValue( [] ) );
585
586 $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
587
588 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
589 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
590 $user,
591 [ 'filters' => [ $filtersOption ] ]
592 );
593
594 $this->assertEmpty( $items );
595 }
596
597 public function mysqlIndexOptimizationProvider() {
598 return [
599 [
600 'mysql',
601 [],
602 [ "rc_timestamp > ''" ],
603 ],
604 [
605 'mysql',
606 [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
607 [ "rc_timestamp <= '20151212010101'" ],
608 ],
609 [
610 'mysql',
611 [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
612 [ "rc_timestamp >= '20151212010101'" ],
613 ],
614 [
615 'postgres',
616 [],
617 [],
618 ],
619 ];
620 }
621
622 /**
623 * @dataProvider mysqlIndexOptimizationProvider
624 */
625 public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
626 $dbType,
627 array $options,
628 array $expectedExtraConds
629 ) {
630 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
631 $conds = array_merge( $commonConds, $expectedExtraConds );
632
633 $mockDb = $this->getMockDb();
634 $mockDb->expects( $this->once() )
635 ->method( 'select' )
636 ->with(
637 [ 'recentchanges', 'watchlist', 'page' ],
638 $this->isType( 'array' ),
639 $conds,
640 $this->isType( 'string' ),
641 $this->isType( 'array' ),
642 $this->isType( 'array' )
643 )
644 ->will( $this->returnValue( [] ) );
645 $mockDb->expects( $this->any() )
646 ->method( 'getType' )
647 ->will( $this->returnValue( $dbType ) );
648
649 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
650 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
651
652 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
653
654 $this->assertEmpty( $items );
655 }
656
657 public function userPermissionRelatedExtraChecksProvider() {
658 return [
659 [
660 [],
661 'deletedhistory',
662 [
663 '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
664 LogPage::DELETED_ACTION . ')'
665 ],
666 ],
667 [
668 [],
669 'suppressrevision',
670 [
671 '(rc_type != ' . RC_LOG . ') OR (' .
672 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
673 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
674 ],
675 ],
676 [
677 [],
678 'viewsuppressed',
679 [
680 '(rc_type != ' . RC_LOG . ') OR (' .
681 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
682 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
683 ],
684 ],
685 [
686 [ 'onlyByUser' => 'SomeOtherUser' ],
687 'deletedhistory',
688 [
689 'rc_user_text' => 'SomeOtherUser',
690 '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
691 '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
692 LogPage::DELETED_ACTION . ')'
693 ],
694 ],
695 [
696 [ 'onlyByUser' => 'SomeOtherUser' ],
697 'suppressrevision',
698 [
699 'rc_user_text' => 'SomeOtherUser',
700 '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
701 ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
702 '(rc_type != ' . RC_LOG . ') OR (' .
703 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
704 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
705 ],
706 ],
707 [
708 [ 'onlyByUser' => 'SomeOtherUser' ],
709 'viewsuppressed',
710 [
711 'rc_user_text' => 'SomeOtherUser',
712 '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
713 ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
714 '(rc_type != ' . RC_LOG . ') OR (' .
715 '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
716 ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
717 ],
718 ],
719 ];
720 }
721
722 /**
723 * @dataProvider userPermissionRelatedExtraChecksProvider
724 */
725 public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
726 array $options,
727 $notAllowedAction,
728 array $expectedExtraConds
729 ) {
730 $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
731 $conds = array_merge( $commonConds, $expectedExtraConds );
732
733 $mockDb = $this->getMockDb();
734 $mockDb->expects( $this->once() )
735 ->method( 'select' )
736 ->with(
737 [ 'recentchanges', 'watchlist', 'page' ],
738 $this->isType( 'array' ),
739 $conds,
740 $this->isType( 'string' ),
741 $this->isType( 'array' ),
742 $this->isType( 'array' )
743 )
744 ->will( $this->returnValue( [] ) );
745
746 $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
747
748 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
749 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
750
751 $this->assertEmpty( $items );
752 }
753
754 public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
755 $mockDb = $this->getMockDb();
756 $mockDb->expects( $this->once() )
757 ->method( 'select' )
758 ->with(
759 [ 'recentchanges', 'watchlist' ],
760 [
761 'rc_id',
762 'rc_namespace',
763 'rc_title',
764 'rc_timestamp',
765 'rc_type',
766 'rc_deleted',
767 'wl_notificationtimestamp',
768
769 'rc_cur_id',
770 'rc_this_oldid',
771 'rc_last_oldid',
772 ],
773 [ 'wl_user' => 1, ],
774 $this->isType( 'string' ),
775 [],
776 [
777 'watchlist' => [
778 'INNER JOIN',
779 [
780 'wl_namespace=rc_namespace',
781 'wl_title=rc_title'
782 ]
783 ],
784 ]
785 )
786 ->will( $this->returnValue( [] ) );
787
788 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
789 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
790
791 $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
792
793 $this->assertEmpty( $items );
794 }
795
796 public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
797 return [
798 [
799 [ 'rcTypes' => [ 1337 ] ],
800 'Bad value for parameter $options[\'rcTypes\']',
801 ],
802 [
803 [ 'rcTypes' => [ 'edit' ] ],
804 'Bad value for parameter $options[\'rcTypes\']',
805 ],
806 [
807 [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
808 'Bad value for parameter $options[\'rcTypes\']',
809 ],
810 [
811 [ 'dir' => 'foo' ],
812 'Bad value for parameter $options[\'dir\']',
813 ],
814 [
815 [ 'start' => '20151212010101' ],
816 'Bad value for parameter $options[\'dir\']: must be provided',
817 ],
818 [
819 [ 'end' => '20151212010101' ],
820 'Bad value for parameter $options[\'dir\']: must be provided',
821 ],
822 [
823 [ 'startFrom' => [ '20151212010101', 123 ] ],
824 'Bad value for parameter $options[\'dir\']: must be provided',
825 ],
826 [
827 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => '20151212010101' ],
828 'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
829 ],
830 [
831 [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => [ '20151212010101' ] ],
832 'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
833 ],
834 [
835 [
836 'dir' => WatchedItemQueryService::DIR_OLDER,
837 'startFrom' => [ '20151212010101', 123, 'foo' ]
838 ],
839 'Bad value for parameter $options[\'startFrom\']: must be a two-element array',
840 ],
841 [
842 [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
843 'Bad value for parameter $options[\'watchlistOwnerToken\']',
844 ],
845 [
846 [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
847 'Bad value for parameter $options[\'watchlistOwner\']',
848 ],
849 ];
850 }
851
852 /**
853 * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
854 */
855 public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
856 array $options,
857 $expectedInExceptionMessage
858 ) {
859 $mockDb = $this->getMockDb();
860 $mockDb->expects( $this->never() )
861 ->method( $this->anything() );
862
863 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
864 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
865
866 $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
867 $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
868 }
869
870 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
871 $mockDb = $this->getMockDb();
872 $mockDb->expects( $this->once() )
873 ->method( 'select' )
874 ->with(
875 [ 'recentchanges', 'watchlist', 'page' ],
876 [
877 'rc_id',
878 'rc_namespace',
879 'rc_title',
880 'rc_timestamp',
881 'rc_type',
882 'rc_deleted',
883 'wl_notificationtimestamp',
884 'rc_cur_id',
885 ],
886 [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
887 $this->isType( 'string' ),
888 [],
889 [
890 'watchlist' => [
891 'INNER JOIN',
892 [
893 'wl_namespace=rc_namespace',
894 'wl_title=rc_title'
895 ]
896 ],
897 'page' => [
898 'LEFT JOIN',
899 'rc_cur_id=page_id',
900 ],
901 ]
902 )
903 ->will( $this->returnValue( [] ) );
904
905 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
906 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
907
908 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
909 $user,
910 [ 'usedInGenerator' => true ]
911 );
912
913 $this->assertEmpty( $items );
914 }
915
916 public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
917 $mockDb = $this->getMockDb();
918 $mockDb->expects( $this->once() )
919 ->method( 'select' )
920 ->with(
921 [ 'recentchanges', 'watchlist' ],
922 [
923 'rc_id',
924 'rc_namespace',
925 'rc_title',
926 'rc_timestamp',
927 'rc_type',
928 'rc_deleted',
929 'wl_notificationtimestamp',
930 'rc_this_oldid',
931 ],
932 [ 'wl_user' => 1 ],
933 $this->isType( 'string' ),
934 [],
935 [
936 'watchlist' => [
937 'INNER JOIN',
938 [
939 'wl_namespace=rc_namespace',
940 'wl_title=rc_title'
941 ]
942 ],
943 ]
944 )
945 ->will( $this->returnValue( [] ) );
946
947 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
948 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
949
950 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
951 $user,
952 [ 'usedInGenerator' => true, 'allRevisions' => true, ]
953 );
954
955 $this->assertEmpty( $items );
956 }
957
958 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
959 $mockDb = $this->getMockDb();
960 $mockDb->expects( $this->once() )
961 ->method( 'select' )
962 ->with(
963 $this->isType( 'array' ),
964 $this->isType( 'array' ),
965 [
966 'wl_user' => 2,
967 '(rc_this_oldid=page_latest) OR (rc_type=3)',
968 ],
969 $this->isType( 'string' ),
970 $this->isType( 'array' ),
971 $this->isType( 'array' )
972 )
973 ->will( $this->returnValue( [] ) );
974
975 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
976 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
977 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
978 $otherUser->expects( $this->once() )
979 ->method( 'getOption' )
980 ->with( 'watchlisttoken' )
981 ->willReturn( '0123456789abcdef' );
982
983 $items = $queryService->getWatchedItemsWithRecentChangeInfo(
984 $user,
985 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
986 );
987
988 $this->assertEmpty( $items );
989 }
990
991 public function invalidWatchlistTokenProvider() {
992 return [
993 [ 'wrongToken' ],
994 [ '' ],
995 ];
996 }
997
998 /**
999 * @dataProvider invalidWatchlistTokenProvider
1000 */
1001 public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
1002 $mockDb = $this->getMockDb();
1003 $mockDb->expects( $this->never() )
1004 ->method( $this->anything() );
1005
1006 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1007 $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
1008 $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1009 $otherUser->expects( $this->once() )
1010 ->method( 'getOption' )
1011 ->with( 'watchlisttoken' )
1012 ->willReturn( '0123456789abcdef' );
1013
1014 $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
1015 $queryService->getWatchedItemsWithRecentChangeInfo(
1016 $user,
1017 [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1018 );
1019 }
1020
1021 public function testGetWatchedItemsForUser() {
1022 $mockDb = $this->getMockDb();
1023 $mockDb->expects( $this->once() )
1024 ->method( 'select' )
1025 ->with(
1026 'watchlist',
1027 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1028 [ 'wl_user' => 1 ]
1029 )
1030 ->will( $this->returnValue( [
1031 $this->getFakeRow( [
1032 'wl_namespace' => 0,
1033 'wl_title' => 'Foo1',
1034 'wl_notificationtimestamp' => '20151212010101',
1035 ] ),
1036 $this->getFakeRow( [
1037 'wl_namespace' => 1,
1038 'wl_title' => 'Foo2',
1039 'wl_notificationtimestamp' => null,
1040 ] ),
1041 ] ) );
1042
1043 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1044 $user = $this->getMockNonAnonUserWithId( 1 );
1045
1046 $items = $queryService->getWatchedItemsForUser( $user );
1047
1048 $this->assertInternalType( 'array', $items );
1049 $this->assertCount( 2, $items );
1050 $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
1051 $this->assertEquals(
1052 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1053 $items[0]
1054 );
1055 $this->assertEquals(
1056 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1057 $items[1]
1058 );
1059 }
1060
1061 public function provideGetWatchedItemsForUserOptions() {
1062 return [
1063 [
1064 [ 'namespaceIds' => [ 0, 1 ], ],
1065 [ 'wl_namespace' => [ 0, 1 ], ],
1066 []
1067 ],
1068 [
1069 [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
1070 [],
1071 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1072 ],
1073 [
1074 [
1075 'namespaceIds' => [ 0 ],
1076 'sort' => WatchedItemQueryService::SORT_ASC,
1077 ],
1078 [ 'wl_namespace' => [ 0 ], ],
1079 [ 'ORDER BY' => 'wl_title ASC' ]
1080 ],
1081 [
1082 [ 'limit' => 10 ],
1083 [],
1084 [ 'LIMIT' => 10 ]
1085 ],
1086 [
1087 [
1088 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1089 'limit' => "10; DROP TABLE watchlist;\n--",
1090 ],
1091 [ 'wl_namespace' => [ 0, 1 ], ],
1092 [ 'LIMIT' => 10 ]
1093 ],
1094 [
1095 [ 'filter' => WatchedItemQueryService::FILTER_CHANGED ],
1096 [ 'wl_notificationtimestamp IS NOT NULL' ],
1097 []
1098 ],
1099 [
1100 [ 'filter' => WatchedItemQueryService::FILTER_NOT_CHANGED ],
1101 [ 'wl_notificationtimestamp IS NULL' ],
1102 []
1103 ],
1104 [
1105 [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
1106 [],
1107 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1108 ],
1109 [
1110 [
1111 'namespaceIds' => [ 0 ],
1112 'sort' => WatchedItemQueryService::SORT_DESC,
1113 ],
1114 [ 'wl_namespace' => [ 0 ], ],
1115 [ 'ORDER BY' => 'wl_title DESC' ]
1116 ],
1117 ];
1118 }
1119
1120 /**
1121 * @dataProvider provideGetWatchedItemsForUserOptions
1122 */
1123 public function testGetWatchedItemsForUser_optionsAndEmptyResult(
1124 array $options,
1125 array $expectedConds,
1126 array $expectedDbOptions
1127 ) {
1128 $mockDb = $this->getMockDb();
1129 $user = $this->getMockNonAnonUserWithId( 1 );
1130
1131 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1132 $mockDb->expects( $this->once() )
1133 ->method( 'select' )
1134 ->with(
1135 'watchlist',
1136 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1137 $expectedConds,
1138 $this->isType( 'string' ),
1139 $expectedDbOptions
1140 )
1141 ->will( $this->returnValue( [] ) );
1142
1143 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1144
1145 $items = $queryService->getWatchedItemsForUser( $user, $options );
1146 $this->assertEmpty( $items );
1147 }
1148
1149 public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
1150 return [
1151 [
1152 [
1153 'from' => new TitleValue( 0, 'SomeDbKey' ),
1154 'sort' => WatchedItemQueryService::SORT_ASC
1155 ],
1156 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1157 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1158 ],
1159 [
1160 [
1161 'from' => new TitleValue( 0, 'SomeDbKey' ),
1162 'sort' => WatchedItemQueryService::SORT_DESC,
1163 ],
1164 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1165 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1166 ],
1167 [
1168 [
1169 'until' => new TitleValue( 0, 'SomeDbKey' ),
1170 'sort' => WatchedItemQueryService::SORT_ASC
1171 ],
1172 [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1173 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1174 ],
1175 [
1176 [
1177 'until' => new TitleValue( 0, 'SomeDbKey' ),
1178 'sort' => WatchedItemQueryService::SORT_DESC
1179 ],
1180 [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1181 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1182 ],
1183 [
1184 [
1185 'from' => new TitleValue( 0, 'AnotherDbKey' ),
1186 'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1187 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1188 'sort' => WatchedItemQueryService::SORT_ASC
1189 ],
1190 [
1191 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1192 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1193 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1194 ],
1195 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1196 ],
1197 [
1198 [
1199 'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1200 'until' => new TitleValue( 0, 'AnotherDbKey' ),
1201 'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1202 'sort' => WatchedItemQueryService::SORT_DESC
1203 ],
1204 [
1205 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1206 "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1207 "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1208 ],
1209 [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1210 ],
1211 ];
1212 }
1213
1214 /**
1215 * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
1216 */
1217 public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
1218 array $options,
1219 array $expectedConds,
1220 array $expectedDbOptions
1221 ) {
1222 $user = $this->getMockNonAnonUserWithId( 1 );
1223
1224 $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1225
1226 $mockDb = $this->getMockDb();
1227 $mockDb->expects( $this->any() )
1228 ->method( 'addQuotes' )
1229 ->will( $this->returnCallback( function( $value ) {
1230 return "'$value'";
1231 } ) );
1232 $mockDb->expects( $this->any() )
1233 ->method( 'makeList' )
1234 ->with(
1235 $this->isType( 'array' ),
1236 $this->isType( 'int' )
1237 )
1238 ->will( $this->returnCallback( function( $a, $conj ) {
1239 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
1240 return join( $sqlConj, array_map( function( $s ) {
1241 return '(' . $s . ')';
1242 }, $a
1243 ) );
1244 } ) );
1245 $mockDb->expects( $this->once() )
1246 ->method( 'select' )
1247 ->with(
1248 'watchlist',
1249 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1250 $expectedConds,
1251 $this->isType( 'string' ),
1252 $expectedDbOptions
1253 )
1254 ->will( $this->returnValue( [] ) );
1255
1256 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1257
1258 $items = $queryService->getWatchedItemsForUser( $user, $options );
1259 $this->assertEmpty( $items );
1260 }
1261
1262 public function getWatchedItemsForUserInvalidOptionsProvider() {
1263 return [
1264 [
1265 [ 'sort' => 'foo' ],
1266 'Bad value for parameter $options[\'sort\']'
1267 ],
1268 [
1269 [ 'filter' => 'foo' ],
1270 'Bad value for parameter $options[\'filter\']'
1271 ],
1272 [
1273 [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1274 'Bad value for parameter $options[\'sort\']: must be provided'
1275 ],
1276 [
1277 [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1278 'Bad value for parameter $options[\'sort\']: must be provided'
1279 ],
1280 [
1281 [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1282 'Bad value for parameter $options[\'sort\']: must be provided'
1283 ],
1284 ];
1285 }
1286
1287 /**
1288 * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
1289 */
1290 public function testGetWatchedItemsForUser_invalidOptionThrowsException(
1291 array $options,
1292 $expectedInExceptionMessage
1293 ) {
1294 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
1295
1296 $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1297 $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1298 }
1299
1300 public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
1301 $mockDb = $this->getMockDb();
1302
1303 $mockDb->expects( $this->never() )
1304 ->method( $this->anything() );
1305
1306 $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
1307
1308 $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1309 $this->assertEmpty( $items );
1310 }
1311
1312 }