Add @covers for RemexStripTagHandler
[lhc/web/wiklou.git] / tests / phpunit / includes / watcheditem / WatchedItemStoreUnitTest.php
1 <?php
2 use MediaWiki\Linker\LinkTarget;
3 use Wikimedia\Rdbms\LoadBalancer;
4 use Wikimedia\ScopedCallback;
5
6 /**
7 * @author Addshore
8 *
9 * @covers WatchedItemStore
10 */
11 class WatchedItemStoreUnitTest extends MediaWikiTestCase {
12
13 /**
14 * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
15 */
16 private function getMockDb() {
17 return $this->createMock( IDatabase::class );
18 }
19
20 /**
21 * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
22 */
23 private function getMockLoadBalancer(
24 $mockDb,
25 $expectedConnectionType = null
26 ) {
27 $mock = $this->getMockBuilder( LoadBalancer::class )
28 ->disableOriginalConstructor()
29 ->getMock();
30 if ( $expectedConnectionType !== null ) {
31 $mock->expects( $this->any() )
32 ->method( 'getConnectionRef' )
33 ->with( $expectedConnectionType )
34 ->will( $this->returnValue( $mockDb ) );
35 } else {
36 $mock->expects( $this->any() )
37 ->method( 'getConnectionRef' )
38 ->will( $this->returnValue( $mockDb ) );
39 }
40 return $mock;
41 }
42
43 /**
44 * @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff
45 */
46 private function getMockCache() {
47 $mock = $this->getMockBuilder( HashBagOStuff::class )
48 ->disableOriginalConstructor()
49 ->getMock();
50 $mock->expects( $this->any() )
51 ->method( 'makeKey' )
52 ->will( $this->returnCallback( function () {
53 return implode( ':', func_get_args() );
54 } ) );
55 return $mock;
56 }
57
58 /**
59 * @return PHPUnit_Framework_MockObject_MockObject|ReadOnlyMode
60 */
61 private function getMockReadOnlyMode( $readOnly = false ) {
62 $mock = $this->getMockBuilder( ReadOnlyMode::class )
63 ->disableOriginalConstructor()
64 ->getMock();
65 $mock->expects( $this->any() )
66 ->method( 'isReadOnly' )
67 ->will( $this->returnValue( $readOnly ) );
68 return $mock;
69 }
70
71 /**
72 * @param int $id
73 * @return PHPUnit_Framework_MockObject_MockObject|User
74 */
75 private function getMockNonAnonUserWithId( $id ) {
76 $mock = $this->createMock( User::class );
77 $mock->expects( $this->any() )
78 ->method( 'isAnon' )
79 ->will( $this->returnValue( false ) );
80 $mock->expects( $this->any() )
81 ->method( 'getId' )
82 ->will( $this->returnValue( $id ) );
83 return $mock;
84 }
85
86 /**
87 * @return User
88 */
89 private function getAnonUser() {
90 return User::newFromName( 'Anon_User' );
91 }
92
93 private function getFakeRow( array $rowValues ) {
94 $fakeRow = new stdClass();
95 foreach ( $rowValues as $valueName => $value ) {
96 $fakeRow->$valueName = $value;
97 }
98 return $fakeRow;
99 }
100
101 private function newWatchedItemStore( LoadBalancer $loadBalancer, HashBagOStuff $cache,
102 ReadOnlyMode $readOnlyMode
103 ) {
104 return new WatchedItemStore(
105 $loadBalancer,
106 $cache,
107 $readOnlyMode
108 );
109 }
110
111 public function testCountWatchedItems() {
112 $user = $this->getMockNonAnonUserWithId( 1 );
113
114 $mockDb = $this->getMockDb();
115 $mockDb->expects( $this->exactly( 1 ) )
116 ->method( 'selectField' )
117 ->with(
118 'watchlist',
119 'COUNT(*)',
120 [
121 'wl_user' => $user->getId(),
122 ],
123 $this->isType( 'string' )
124 )
125 ->will( $this->returnValue( '12' ) );
126
127 $mockCache = $this->getMockCache();
128 $mockCache->expects( $this->never() )->method( 'get' );
129 $mockCache->expects( $this->never() )->method( 'set' );
130 $mockCache->expects( $this->never() )->method( 'delete' );
131
132 $store = $this->newWatchedItemStore(
133 $this->getMockLoadBalancer( $mockDb ),
134 $mockCache,
135 $this->getMockReadOnlyMode()
136 );
137
138 $this->assertEquals( 12, $store->countWatchedItems( $user ) );
139 }
140
141 public function testCountWatchers() {
142 $titleValue = new TitleValue( 0, 'SomeDbKey' );
143
144 $mockDb = $this->getMockDb();
145 $mockDb->expects( $this->exactly( 1 ) )
146 ->method( 'selectField' )
147 ->with(
148 'watchlist',
149 'COUNT(*)',
150 [
151 'wl_namespace' => $titleValue->getNamespace(),
152 'wl_title' => $titleValue->getDBkey(),
153 ],
154 $this->isType( 'string' )
155 )
156 ->will( $this->returnValue( '7' ) );
157
158 $mockCache = $this->getMockCache();
159 $mockCache->expects( $this->never() )->method( 'get' );
160 $mockCache->expects( $this->never() )->method( 'set' );
161 $mockCache->expects( $this->never() )->method( 'delete' );
162
163 $store = $this->newWatchedItemStore(
164 $this->getMockLoadBalancer( $mockDb ),
165 $mockCache,
166 $this->getMockReadOnlyMode()
167 );
168
169 $this->assertEquals( 7, $store->countWatchers( $titleValue ) );
170 }
171
172 public function testCountWatchersMultiple() {
173 $titleValues = [
174 new TitleValue( 0, 'SomeDbKey' ),
175 new TitleValue( 0, 'OtherDbKey' ),
176 new TitleValue( 1, 'AnotherDbKey' ),
177 ];
178
179 $mockDb = $this->getMockDb();
180
181 $dbResult = [
182 $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ),
183 $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ),
184 $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ]
185 ),
186 ];
187 $mockDb->expects( $this->once() )
188 ->method( 'makeWhereFrom2d' )
189 ->with(
190 [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
191 $this->isType( 'string' ),
192 $this->isType( 'string' )
193 )
194 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
195 $mockDb->expects( $this->once() )
196 ->method( 'select' )
197 ->with(
198 'watchlist',
199 [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
200 [ 'makeWhereFrom2d return value' ],
201 $this->isType( 'string' ),
202 [
203 'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
204 ]
205 )
206 ->will(
207 $this->returnValue( $dbResult )
208 );
209
210 $mockCache = $this->getMockCache();
211 $mockCache->expects( $this->never() )->method( 'get' );
212 $mockCache->expects( $this->never() )->method( 'set' );
213 $mockCache->expects( $this->never() )->method( 'delete' );
214
215 $store = $this->newWatchedItemStore(
216 $this->getMockLoadBalancer( $mockDb ),
217 $mockCache,
218 $this->getMockReadOnlyMode()
219 );
220
221 $expected = [
222 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
223 1 => [ 'AnotherDbKey' => 500 ],
224 ];
225 $this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) );
226 }
227
228 public function provideIntWithDbUnsafeVersion() {
229 return [
230 [ 50 ],
231 [ "50; DROP TABLE watchlist;\n--" ],
232 ];
233 }
234
235 /**
236 * @dataProvider provideIntWithDbUnsafeVersion
237 */
238 public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers ) {
239 $titleValues = [
240 new TitleValue( 0, 'SomeDbKey' ),
241 new TitleValue( 0, 'OtherDbKey' ),
242 new TitleValue( 1, 'AnotherDbKey' ),
243 ];
244
245 $mockDb = $this->getMockDb();
246
247 $dbResult = [
248 $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ),
249 $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ),
250 $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ]
251 ),
252 ];
253 $mockDb->expects( $this->once() )
254 ->method( 'makeWhereFrom2d' )
255 ->with(
256 [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
257 $this->isType( 'string' ),
258 $this->isType( 'string' )
259 )
260 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
261 $mockDb->expects( $this->once() )
262 ->method( 'select' )
263 ->with(
264 'watchlist',
265 [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
266 [ 'makeWhereFrom2d return value' ],
267 $this->isType( 'string' ),
268 [
269 'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
270 'HAVING' => 'COUNT(*) >= 50',
271 ]
272 )
273 ->will(
274 $this->returnValue( $dbResult )
275 );
276
277 $mockCache = $this->getMockCache();
278 $mockCache->expects( $this->never() )->method( 'get' );
279 $mockCache->expects( $this->never() )->method( 'set' );
280 $mockCache->expects( $this->never() )->method( 'delete' );
281
282 $store = $this->newWatchedItemStore(
283 $this->getMockLoadBalancer( $mockDb ),
284 $mockCache,
285 $this->getMockReadOnlyMode()
286 );
287
288 $expected = [
289 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
290 1 => [ 'AnotherDbKey' => 500 ],
291 ];
292 $this->assertEquals(
293 $expected,
294 $store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] )
295 );
296 }
297
298 public function testCountVisitingWatchers() {
299 $titleValue = new TitleValue( 0, 'SomeDbKey' );
300
301 $mockDb = $this->getMockDb();
302 $mockDb->expects( $this->exactly( 1 ) )
303 ->method( 'selectField' )
304 ->with(
305 'watchlist',
306 'COUNT(*)',
307 [
308 'wl_namespace' => $titleValue->getNamespace(),
309 'wl_title' => $titleValue->getDBkey(),
310 'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL',
311 ],
312 $this->isType( 'string' )
313 )
314 ->will( $this->returnValue( '7' ) );
315 $mockDb->expects( $this->exactly( 1 ) )
316 ->method( 'addQuotes' )
317 ->will( $this->returnCallback( function ( $value ) {
318 return "'$value'";
319 } ) );
320 $mockDb->expects( $this->exactly( 1 ) )
321 ->method( 'timestamp' )
322 ->will( $this->returnCallback( function ( $value ) {
323 return 'TS' . $value . 'TS';
324 } ) );
325
326 $mockCache = $this->getMockCache();
327 $mockCache->expects( $this->never() )->method( 'set' );
328 $mockCache->expects( $this->never() )->method( 'get' );
329 $mockCache->expects( $this->never() )->method( 'delete' );
330
331 $store = $this->newWatchedItemStore(
332 $this->getMockLoadBalancer( $mockDb ),
333 $mockCache,
334 $this->getMockReadOnlyMode()
335 );
336
337 $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
338 }
339
340 public function testCountVisitingWatchersMultiple() {
341 $titleValuesWithThresholds = [
342 [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
343 [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
344 [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
345 ];
346
347 $dbResult = [
348 $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ),
349 $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ),
350 $this->getFakeRow(
351 [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ]
352 ),
353 ];
354 $mockDb = $this->getMockDb();
355 $mockDb->expects( $this->exactly( 2 * 3 ) )
356 ->method( 'addQuotes' )
357 ->will( $this->returnCallback( function ( $value ) {
358 return "'$value'";
359 } ) );
360 $mockDb->expects( $this->exactly( 3 ) )
361 ->method( 'timestamp' )
362 ->will( $this->returnCallback( function ( $value ) {
363 return 'TS' . $value . 'TS';
364 } ) );
365 $mockDb->expects( $this->any() )
366 ->method( 'makeList' )
367 ->with(
368 $this->isType( 'array' ),
369 $this->isType( 'int' )
370 )
371 ->will( $this->returnCallback( function ( $a, $conj ) {
372 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
373 return join( $sqlConj, array_map( function ( $s ) {
374 return '(' . $s . ')';
375 }, $a
376 ) );
377 } ) );
378 $mockDb->expects( $this->never() )
379 ->method( 'makeWhereFrom2d' );
380
381 $expectedCond =
382 '((wl_namespace = 0) AND (' .
383 "(((wl_title = 'SomeDbKey') AND (" .
384 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
385 ')) OR (' .
386 "(wl_title = 'OtherDbKey') AND (" .
387 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
388 '))))' .
389 ') OR ((wl_namespace = 1) AND (' .
390 "(((wl_title = 'AnotherDbKey') AND (".
391 "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
392 ')))))';
393 $mockDb->expects( $this->once() )
394 ->method( 'select' )
395 ->with(
396 'watchlist',
397 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
398 $expectedCond,
399 $this->isType( 'string' ),
400 [
401 'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
402 ]
403 )
404 ->will(
405 $this->returnValue( $dbResult )
406 );
407
408 $mockCache = $this->getMockCache();
409 $mockCache->expects( $this->never() )->method( 'get' );
410 $mockCache->expects( $this->never() )->method( 'set' );
411 $mockCache->expects( $this->never() )->method( 'delete' );
412
413 $store = $this->newWatchedItemStore(
414 $this->getMockLoadBalancer( $mockDb ),
415 $mockCache,
416 $this->getMockReadOnlyMode()
417 );
418
419 $expected = [
420 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
421 1 => [ 'AnotherDbKey' => 500 ],
422 ];
423 $this->assertEquals(
424 $expected,
425 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
426 );
427 }
428
429 public function testCountVisitingWatchersMultiple_withMissingTargets() {
430 $titleValuesWithThresholds = [
431 [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
432 [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
433 [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
434 [ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ],
435 [ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ],
436 ];
437
438 $dbResult = [
439 $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ] ),
440 $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ] ),
441 $this->getFakeRow(
442 [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ]
443 ),
444 $this->getFakeRow(
445 [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '100' ]
446 ),
447 $this->getFakeRow(
448 [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '200' ]
449 ),
450 ];
451 $mockDb = $this->getMockDb();
452 $mockDb->expects( $this->exactly( 2 * 3 ) )
453 ->method( 'addQuotes' )
454 ->will( $this->returnCallback( function ( $value ) {
455 return "'$value'";
456 } ) );
457 $mockDb->expects( $this->exactly( 3 ) )
458 ->method( 'timestamp' )
459 ->will( $this->returnCallback( function ( $value ) {
460 return 'TS' . $value . 'TS';
461 } ) );
462 $mockDb->expects( $this->any() )
463 ->method( 'makeList' )
464 ->with(
465 $this->isType( 'array' ),
466 $this->isType( 'int' )
467 )
468 ->will( $this->returnCallback( function ( $a, $conj ) {
469 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
470 return join( $sqlConj, array_map( function ( $s ) {
471 return '(' . $s . ')';
472 }, $a
473 ) );
474 } ) );
475 $mockDb->expects( $this->once() )
476 ->method( 'makeWhereFrom2d' )
477 ->with(
478 [ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ],
479 $this->isType( 'string' ),
480 $this->isType( 'string' )
481 )
482 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
483
484 $expectedCond =
485 '((wl_namespace = 0) AND (' .
486 "(((wl_title = 'SomeDbKey') AND (" .
487 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
488 ')) OR (' .
489 "(wl_title = 'OtherDbKey') AND (" .
490 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
491 '))))' .
492 ') OR ((wl_namespace = 1) AND (' .
493 "(((wl_title = 'AnotherDbKey') AND (".
494 "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
495 '))))' .
496 ') OR ' .
497 '(makeWhereFrom2d return value)';
498 $mockDb->expects( $this->once() )
499 ->method( 'select' )
500 ->with(
501 'watchlist',
502 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
503 $expectedCond,
504 $this->isType( 'string' ),
505 [
506 'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
507 ]
508 )
509 ->will(
510 $this->returnValue( $dbResult )
511 );
512
513 $mockCache = $this->getMockCache();
514 $mockCache->expects( $this->never() )->method( 'get' );
515 $mockCache->expects( $this->never() )->method( 'set' );
516 $mockCache->expects( $this->never() )->method( 'delete' );
517
518 $store = $this->newWatchedItemStore(
519 $this->getMockLoadBalancer( $mockDb ),
520 $mockCache,
521 $this->getMockReadOnlyMode()
522 );
523
524 $expected = [
525 0 => [
526 'SomeDbKey' => 100, 'OtherDbKey' => 300,
527 'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200
528 ],
529 1 => [ 'AnotherDbKey' => 500 ],
530 ];
531 $this->assertEquals(
532 $expected,
533 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
534 );
535 }
536
537 /**
538 * @dataProvider provideIntWithDbUnsafeVersion
539 */
540 public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers ) {
541 $titleValuesWithThresholds = [
542 [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
543 [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
544 [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
545 ];
546
547 $mockDb = $this->getMockDb();
548 $mockDb->expects( $this->any() )
549 ->method( 'makeList' )
550 ->will( $this->returnValue( 'makeList return value' ) );
551 $mockDb->expects( $this->once() )
552 ->method( 'select' )
553 ->with(
554 'watchlist',
555 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
556 'makeList return value',
557 $this->isType( 'string' ),
558 [
559 'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
560 'HAVING' => 'COUNT(*) >= 50',
561 ]
562 )
563 ->will(
564 $this->returnValue( [] )
565 );
566
567 $mockCache = $this->getMockCache();
568 $mockCache->expects( $this->never() )->method( 'get' );
569 $mockCache->expects( $this->never() )->method( 'set' );
570 $mockCache->expects( $this->never() )->method( 'delete' );
571
572 $store = $this->newWatchedItemStore(
573 $this->getMockLoadBalancer( $mockDb ),
574 $mockCache,
575 $this->getMockReadOnlyMode()
576 );
577
578 $expected = [
579 0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
580 1 => [ 'AnotherDbKey' => 0 ],
581 ];
582 $this->assertEquals(
583 $expected,
584 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers )
585 );
586 }
587
588 public function testCountUnreadNotifications() {
589 $user = $this->getMockNonAnonUserWithId( 1 );
590
591 $mockDb = $this->getMockDb();
592 $mockDb->expects( $this->exactly( 1 ) )
593 ->method( 'selectRowCount' )
594 ->with(
595 'watchlist',
596 '1',
597 [
598 "wl_notificationtimestamp IS NOT NULL",
599 'wl_user' => 1,
600 ],
601 $this->isType( 'string' )
602 )
603 ->will( $this->returnValue( '9' ) );
604
605 $mockCache = $this->getMockCache();
606 $mockCache->expects( $this->never() )->method( 'set' );
607 $mockCache->expects( $this->never() )->method( 'get' );
608 $mockCache->expects( $this->never() )->method( 'delete' );
609
610 $store = $this->newWatchedItemStore(
611 $this->getMockLoadBalancer( $mockDb ),
612 $mockCache,
613 $this->getMockReadOnlyMode()
614 );
615
616 $this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
617 }
618
619 /**
620 * @dataProvider provideIntWithDbUnsafeVersion
621 */
622 public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) {
623 $user = $this->getMockNonAnonUserWithId( 1 );
624
625 $mockDb = $this->getMockDb();
626 $mockDb->expects( $this->exactly( 1 ) )
627 ->method( 'selectRowCount' )
628 ->with(
629 'watchlist',
630 '1',
631 [
632 "wl_notificationtimestamp IS NOT NULL",
633 'wl_user' => 1,
634 ],
635 $this->isType( 'string' ),
636 [ 'LIMIT' => 50 ]
637 )
638 ->will( $this->returnValue( '50' ) );
639
640 $mockCache = $this->getMockCache();
641 $mockCache->expects( $this->never() )->method( 'set' );
642 $mockCache->expects( $this->never() )->method( 'get' );
643 $mockCache->expects( $this->never() )->method( 'delete' );
644
645 $store = $this->newWatchedItemStore(
646 $this->getMockLoadBalancer( $mockDb ),
647 $mockCache,
648 $this->getMockReadOnlyMode()
649 );
650
651 $this->assertSame(
652 true,
653 $store->countUnreadNotifications( $user, $limit )
654 );
655 }
656
657 /**
658 * @dataProvider provideIntWithDbUnsafeVersion
659 */
660 public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) {
661 $user = $this->getMockNonAnonUserWithId( 1 );
662
663 $mockDb = $this->getMockDb();
664 $mockDb->expects( $this->exactly( 1 ) )
665 ->method( 'selectRowCount' )
666 ->with(
667 'watchlist',
668 '1',
669 [
670 "wl_notificationtimestamp IS NOT NULL",
671 'wl_user' => 1,
672 ],
673 $this->isType( 'string' ),
674 [ 'LIMIT' => 50 ]
675 )
676 ->will( $this->returnValue( '9' ) );
677
678 $mockCache = $this->getMockCache();
679 $mockCache->expects( $this->never() )->method( 'set' );
680 $mockCache->expects( $this->never() )->method( 'get' );
681 $mockCache->expects( $this->never() )->method( 'delete' );
682
683 $store = $this->newWatchedItemStore(
684 $this->getMockLoadBalancer( $mockDb ),
685 $mockCache,
686 $this->getMockReadOnlyMode()
687 );
688
689 $this->assertEquals(
690 9,
691 $store->countUnreadNotifications( $user, $limit )
692 );
693 }
694
695 public function testDuplicateEntry_nothingToDuplicate() {
696 $mockDb = $this->getMockDb();
697 $mockDb->expects( $this->once() )
698 ->method( 'select' )
699 ->with(
700 'watchlist',
701 [
702 'wl_user',
703 'wl_notificationtimestamp',
704 ],
705 [
706 'wl_namespace' => 0,
707 'wl_title' => 'Old_Title',
708 ],
709 'WatchedItemStore::duplicateEntry',
710 [ 'FOR UPDATE' ]
711 )
712 ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
713
714 $store = $this->newWatchedItemStore(
715 $this->getMockLoadBalancer( $mockDb ),
716 $this->getMockCache(),
717 $this->getMockReadOnlyMode()
718 );
719
720 $store->duplicateEntry(
721 Title::newFromText( 'Old_Title' ),
722 Title::newFromText( 'New_Title' )
723 );
724 }
725
726 public function testDuplicateEntry_somethingToDuplicate() {
727 $fakeRows = [
728 $this->getFakeRow( [ 'wl_user' => '1', 'wl_notificationtimestamp' => '20151212010101' ] ),
729 $this->getFakeRow( [ 'wl_user' => '2', 'wl_notificationtimestamp' => null ] ),
730 ];
731
732 $mockDb = $this->getMockDb();
733 $mockDb->expects( $this->at( 0 ) )
734 ->method( 'select' )
735 ->with(
736 'watchlist',
737 [
738 'wl_user',
739 'wl_notificationtimestamp',
740 ],
741 [
742 'wl_namespace' => 0,
743 'wl_title' => 'Old_Title',
744 ]
745 )
746 ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
747 $mockDb->expects( $this->at( 1 ) )
748 ->method( 'replace' )
749 ->with(
750 'watchlist',
751 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
752 [
753 [
754 'wl_user' => 1,
755 'wl_namespace' => 0,
756 'wl_title' => 'New_Title',
757 'wl_notificationtimestamp' => '20151212010101',
758 ],
759 [
760 'wl_user' => 2,
761 'wl_namespace' => 0,
762 'wl_title' => 'New_Title',
763 'wl_notificationtimestamp' => null,
764 ],
765 ],
766 $this->isType( 'string' )
767 );
768
769 $mockCache = $this->getMockCache();
770 $mockCache->expects( $this->never() )->method( 'get' );
771 $mockCache->expects( $this->never() )->method( 'delete' );
772
773 $store = $this->newWatchedItemStore(
774 $this->getMockLoadBalancer( $mockDb ),
775 $mockCache,
776 $this->getMockReadOnlyMode()
777 );
778
779 $store->duplicateEntry(
780 Title::newFromText( 'Old_Title' ),
781 Title::newFromText( 'New_Title' )
782 );
783 }
784
785 public function testDuplicateAllAssociatedEntries_nothingToDuplicate() {
786 $mockDb = $this->getMockDb();
787 $mockDb->expects( $this->at( 0 ) )
788 ->method( 'select' )
789 ->with(
790 'watchlist',
791 [
792 'wl_user',
793 'wl_notificationtimestamp',
794 ],
795 [
796 'wl_namespace' => 0,
797 'wl_title' => 'Old_Title',
798 ]
799 )
800 ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
801 $mockDb->expects( $this->at( 1 ) )
802 ->method( 'select' )
803 ->with(
804 'watchlist',
805 [
806 'wl_user',
807 'wl_notificationtimestamp',
808 ],
809 [
810 'wl_namespace' => 1,
811 'wl_title' => 'Old_Title',
812 ]
813 )
814 ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
815
816 $mockCache = $this->getMockCache();
817 $mockCache->expects( $this->never() )->method( 'get' );
818 $mockCache->expects( $this->never() )->method( 'delete' );
819
820 $store = $this->newWatchedItemStore(
821 $this->getMockLoadBalancer( $mockDb ),
822 $mockCache,
823 $this->getMockReadOnlyMode()
824 );
825
826 $store->duplicateAllAssociatedEntries(
827 Title::newFromText( 'Old_Title' ),
828 Title::newFromText( 'New_Title' )
829 );
830 }
831
832 public function provideLinkTargetPairs() {
833 return [
834 [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ],
835 [ new TitleValue( 0, 'Old_Title' ), new TitleValue( 0, 'New_Title' ) ],
836 ];
837 }
838
839 /**
840 * @dataProvider provideLinkTargetPairs
841 */
842 public function testDuplicateAllAssociatedEntries_somethingToDuplicate(
843 LinkTarget $oldTarget,
844 LinkTarget $newTarget
845 ) {
846 $fakeRows = [
847 $this->getFakeRow( [ 'wl_user' => '1', 'wl_notificationtimestamp' => '20151212010101' ] ),
848 ];
849
850 $mockDb = $this->getMockDb();
851 $mockDb->expects( $this->at( 0 ) )
852 ->method( 'select' )
853 ->with(
854 'watchlist',
855 [
856 'wl_user',
857 'wl_notificationtimestamp',
858 ],
859 [
860 'wl_namespace' => $oldTarget->getNamespace(),
861 'wl_title' => $oldTarget->getDBkey(),
862 ]
863 )
864 ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
865 $mockDb->expects( $this->at( 1 ) )
866 ->method( 'replace' )
867 ->with(
868 'watchlist',
869 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
870 [
871 [
872 'wl_user' => 1,
873 'wl_namespace' => $newTarget->getNamespace(),
874 'wl_title' => $newTarget->getDBkey(),
875 'wl_notificationtimestamp' => '20151212010101',
876 ],
877 ],
878 $this->isType( 'string' )
879 );
880 $mockDb->expects( $this->at( 2 ) )
881 ->method( 'select' )
882 ->with(
883 'watchlist',
884 [
885 'wl_user',
886 'wl_notificationtimestamp',
887 ],
888 [
889 'wl_namespace' => $oldTarget->getNamespace() + 1,
890 'wl_title' => $oldTarget->getDBkey(),
891 ]
892 )
893 ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
894 $mockDb->expects( $this->at( 3 ) )
895 ->method( 'replace' )
896 ->with(
897 'watchlist',
898 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
899 [
900 [
901 'wl_user' => 1,
902 'wl_namespace' => $newTarget->getNamespace() + 1,
903 'wl_title' => $newTarget->getDBkey(),
904 'wl_notificationtimestamp' => '20151212010101',
905 ],
906 ],
907 $this->isType( 'string' )
908 );
909
910 $mockCache = $this->getMockCache();
911 $mockCache->expects( $this->never() )->method( 'get' );
912 $mockCache->expects( $this->never() )->method( 'delete' );
913
914 $store = $this->newWatchedItemStore(
915 $this->getMockLoadBalancer( $mockDb ),
916 $mockCache,
917 $this->getMockReadOnlyMode()
918 );
919
920 $store->duplicateAllAssociatedEntries(
921 $oldTarget,
922 $newTarget
923 );
924 }
925
926 public function testAddWatch_nonAnonymousUser() {
927 $mockDb = $this->getMockDb();
928 $mockDb->expects( $this->once() )
929 ->method( 'insert' )
930 ->with(
931 'watchlist',
932 [
933 [
934 'wl_user' => 1,
935 'wl_namespace' => 0,
936 'wl_title' => 'Some_Page',
937 'wl_notificationtimestamp' => null,
938 ]
939 ]
940 );
941
942 $mockCache = $this->getMockCache();
943 $mockCache->expects( $this->once() )
944 ->method( 'delete' )
945 ->with( '0:Some_Page:1' );
946
947 $store = $this->newWatchedItemStore(
948 $this->getMockLoadBalancer( $mockDb ),
949 $mockCache,
950 $this->getMockReadOnlyMode()
951 );
952
953 $store->addWatch(
954 $this->getMockNonAnonUserWithId( 1 ),
955 Title::newFromText( 'Some_Page' )
956 );
957 }
958
959 public function testAddWatch_anonymousUser() {
960 $mockDb = $this->getMockDb();
961 $mockDb->expects( $this->never() )
962 ->method( 'insert' );
963
964 $mockCache = $this->getMockCache();
965 $mockCache->expects( $this->never() )
966 ->method( 'delete' );
967
968 $store = $this->newWatchedItemStore(
969 $this->getMockLoadBalancer( $mockDb ),
970 $mockCache,
971 $this->getMockReadOnlyMode()
972 );
973
974 $store->addWatch(
975 $this->getAnonUser(),
976 Title::newFromText( 'Some_Page' )
977 );
978 }
979
980 public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
981 $store = $this->newWatchedItemStore(
982 $this->getMockLoadBalancer( $this->getMockDb() ),
983 $this->getMockCache(),
984 $this->getMockReadOnlyMode( true )
985 );
986
987 $this->assertFalse(
988 $store->addWatchBatchForUser(
989 $this->getMockNonAnonUserWithId( 1 ),
990 [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
991 )
992 );
993 }
994
995 public function testAddWatchBatchForUser_nonAnonymousUser() {
996 $mockDb = $this->getMockDb();
997 $mockDb->expects( $this->once() )
998 ->method( 'insert' )
999 ->with(
1000 'watchlist',
1001 [
1002 [
1003 'wl_user' => 1,
1004 'wl_namespace' => 0,
1005 'wl_title' => 'Some_Page',
1006 'wl_notificationtimestamp' => null,
1007 ],
1008 [
1009 'wl_user' => 1,
1010 'wl_namespace' => 1,
1011 'wl_title' => 'Some_Page',
1012 'wl_notificationtimestamp' => null,
1013 ]
1014 ]
1015 );
1016
1017 $mockCache = $this->getMockCache();
1018 $mockCache->expects( $this->exactly( 2 ) )
1019 ->method( 'delete' );
1020 $mockCache->expects( $this->at( 1 ) )
1021 ->method( 'delete' )
1022 ->with( '0:Some_Page:1' );
1023 $mockCache->expects( $this->at( 3 ) )
1024 ->method( 'delete' )
1025 ->with( '1:Some_Page:1' );
1026
1027 $store = $this->newWatchedItemStore(
1028 $this->getMockLoadBalancer( $mockDb ),
1029 $mockCache,
1030 $this->getMockReadOnlyMode()
1031 );
1032
1033 $mockUser = $this->getMockNonAnonUserWithId( 1 );
1034
1035 $this->assertTrue(
1036 $store->addWatchBatchForUser(
1037 $mockUser,
1038 [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
1039 )
1040 );
1041 }
1042
1043 public function testAddWatchBatchForUser_anonymousUsersAreSkipped() {
1044 $mockDb = $this->getMockDb();
1045 $mockDb->expects( $this->never() )
1046 ->method( 'insert' );
1047
1048 $mockCache = $this->getMockCache();
1049 $mockCache->expects( $this->never() )
1050 ->method( 'delete' );
1051
1052 $store = $this->newWatchedItemStore(
1053 $this->getMockLoadBalancer( $mockDb ),
1054 $mockCache,
1055 $this->getMockReadOnlyMode()
1056 );
1057
1058 $this->assertFalse(
1059 $store->addWatchBatchForUser(
1060 $this->getAnonUser(),
1061 [ new TitleValue( 0, 'Other_Page' ) ]
1062 )
1063 );
1064 }
1065
1066 public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() {
1067 $user = $this->getMockNonAnonUserWithId( 1 );
1068 $mockDb = $this->getMockDb();
1069 $mockDb->expects( $this->never() )
1070 ->method( 'insert' );
1071
1072 $mockCache = $this->getMockCache();
1073 $mockCache->expects( $this->never() )
1074 ->method( 'delete' );
1075
1076 $store = $this->newWatchedItemStore(
1077 $this->getMockLoadBalancer( $mockDb ),
1078 $mockCache,
1079 $this->getMockReadOnlyMode()
1080 );
1081
1082 $this->assertTrue(
1083 $store->addWatchBatchForUser( $user, [] )
1084 );
1085 }
1086
1087 public function testLoadWatchedItem_existingItem() {
1088 $mockDb = $this->getMockDb();
1089 $mockDb->expects( $this->once() )
1090 ->method( 'selectRow' )
1091 ->with(
1092 'watchlist',
1093 'wl_notificationtimestamp',
1094 [
1095 'wl_user' => 1,
1096 'wl_namespace' => 0,
1097 'wl_title' => 'SomeDbKey',
1098 ]
1099 )
1100 ->will( $this->returnValue(
1101 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
1102 ) );
1103
1104 $mockCache = $this->getMockCache();
1105 $mockCache->expects( $this->once() )
1106 ->method( 'set' )
1107 ->with(
1108 '0:SomeDbKey:1'
1109 );
1110
1111 $store = $this->newWatchedItemStore(
1112 $this->getMockLoadBalancer( $mockDb ),
1113 $mockCache,
1114 $this->getMockReadOnlyMode()
1115 );
1116
1117 $watchedItem = $store->loadWatchedItem(
1118 $this->getMockNonAnonUserWithId( 1 ),
1119 new TitleValue( 0, 'SomeDbKey' )
1120 );
1121 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
1122 $this->assertEquals( 1, $watchedItem->getUser()->getId() );
1123 $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
1124 $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
1125 }
1126
1127 public function testLoadWatchedItem_noItem() {
1128 $mockDb = $this->getMockDb();
1129 $mockDb->expects( $this->once() )
1130 ->method( 'selectRow' )
1131 ->with(
1132 'watchlist',
1133 'wl_notificationtimestamp',
1134 [
1135 'wl_user' => 1,
1136 'wl_namespace' => 0,
1137 'wl_title' => 'SomeDbKey',
1138 ]
1139 )
1140 ->will( $this->returnValue( [] ) );
1141
1142 $mockCache = $this->getMockCache();
1143 $mockCache->expects( $this->never() )->method( 'get' );
1144 $mockCache->expects( $this->never() )->method( 'delete' );
1145
1146 $store = $this->newWatchedItemStore(
1147 $this->getMockLoadBalancer( $mockDb ),
1148 $mockCache,
1149 $this->getMockReadOnlyMode()
1150 );
1151
1152 $this->assertFalse(
1153 $store->loadWatchedItem(
1154 $this->getMockNonAnonUserWithId( 1 ),
1155 new TitleValue( 0, 'SomeDbKey' )
1156 )
1157 );
1158 }
1159
1160 public function testLoadWatchedItem_anonymousUser() {
1161 $mockDb = $this->getMockDb();
1162 $mockDb->expects( $this->never() )
1163 ->method( 'selectRow' );
1164
1165 $mockCache = $this->getMockCache();
1166 $mockCache->expects( $this->never() )->method( 'get' );
1167 $mockCache->expects( $this->never() )->method( 'delete' );
1168
1169 $store = $this->newWatchedItemStore(
1170 $this->getMockLoadBalancer( $mockDb ),
1171 $mockCache,
1172 $this->getMockReadOnlyMode()
1173 );
1174
1175 $this->assertFalse(
1176 $store->loadWatchedItem(
1177 $this->getAnonUser(),
1178 new TitleValue( 0, 'SomeDbKey' )
1179 )
1180 );
1181 }
1182
1183 public function testRemoveWatch_existingItem() {
1184 $mockDb = $this->getMockDb();
1185 $mockDb->expects( $this->once() )
1186 ->method( 'delete' )
1187 ->with(
1188 'watchlist',
1189 [
1190 'wl_user' => 1,
1191 'wl_namespace' => 0,
1192 'wl_title' => 'SomeDbKey',
1193 ]
1194 );
1195 $mockDb->expects( $this->once() )
1196 ->method( 'affectedRows' )
1197 ->will( $this->returnValue( 1 ) );
1198
1199 $mockCache = $this->getMockCache();
1200 $mockCache->expects( $this->never() )->method( 'get' );
1201 $mockCache->expects( $this->once() )
1202 ->method( 'delete' )
1203 ->with( '0:SomeDbKey:1' );
1204
1205 $store = $this->newWatchedItemStore(
1206 $this->getMockLoadBalancer( $mockDb ),
1207 $mockCache,
1208 $this->getMockReadOnlyMode()
1209 );
1210
1211 $this->assertTrue(
1212 $store->removeWatch(
1213 $this->getMockNonAnonUserWithId( 1 ),
1214 new TitleValue( 0, 'SomeDbKey' )
1215 )
1216 );
1217 }
1218
1219 public function testRemoveWatch_noItem() {
1220 $mockDb = $this->getMockDb();
1221 $mockDb->expects( $this->once() )
1222 ->method( 'delete' )
1223 ->with(
1224 'watchlist',
1225 [
1226 'wl_user' => 1,
1227 'wl_namespace' => 0,
1228 'wl_title' => 'SomeDbKey',
1229 ]
1230 );
1231 $mockDb->expects( $this->once() )
1232 ->method( 'affectedRows' )
1233 ->will( $this->returnValue( 0 ) );
1234
1235 $mockCache = $this->getMockCache();
1236 $mockCache->expects( $this->never() )->method( 'get' );
1237 $mockCache->expects( $this->once() )
1238 ->method( 'delete' )
1239 ->with( '0:SomeDbKey:1' );
1240
1241 $store = $this->newWatchedItemStore(
1242 $this->getMockLoadBalancer( $mockDb ),
1243 $mockCache,
1244 $this->getMockReadOnlyMode()
1245 );
1246
1247 $this->assertFalse(
1248 $store->removeWatch(
1249 $this->getMockNonAnonUserWithId( 1 ),
1250 new TitleValue( 0, 'SomeDbKey' )
1251 )
1252 );
1253 }
1254
1255 public function testRemoveWatch_anonymousUser() {
1256 $mockDb = $this->getMockDb();
1257 $mockDb->expects( $this->never() )
1258 ->method( 'delete' );
1259
1260 $mockCache = $this->getMockCache();
1261 $mockCache->expects( $this->never() )->method( 'get' );
1262 $mockCache->expects( $this->never() )
1263 ->method( 'delete' );
1264
1265 $store = $this->newWatchedItemStore(
1266 $this->getMockLoadBalancer( $mockDb ),
1267 $mockCache,
1268 $this->getMockReadOnlyMode()
1269 );
1270
1271 $this->assertFalse(
1272 $store->removeWatch(
1273 $this->getAnonUser(),
1274 new TitleValue( 0, 'SomeDbKey' )
1275 )
1276 );
1277 }
1278
1279 public function testGetWatchedItem_existingItem() {
1280 $mockDb = $this->getMockDb();
1281 $mockDb->expects( $this->once() )
1282 ->method( 'selectRow' )
1283 ->with(
1284 'watchlist',
1285 'wl_notificationtimestamp',
1286 [
1287 'wl_user' => 1,
1288 'wl_namespace' => 0,
1289 'wl_title' => 'SomeDbKey',
1290 ]
1291 )
1292 ->will( $this->returnValue(
1293 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
1294 ) );
1295
1296 $mockCache = $this->getMockCache();
1297 $mockCache->expects( $this->never() )->method( 'delete' );
1298 $mockCache->expects( $this->once() )
1299 ->method( 'get' )
1300 ->with(
1301 '0:SomeDbKey:1'
1302 )
1303 ->will( $this->returnValue( null ) );
1304 $mockCache->expects( $this->once() )
1305 ->method( 'set' )
1306 ->with(
1307 '0:SomeDbKey:1'
1308 );
1309
1310 $store = $this->newWatchedItemStore(
1311 $this->getMockLoadBalancer( $mockDb ),
1312 $mockCache,
1313 $this->getMockReadOnlyMode()
1314 );
1315
1316 $watchedItem = $store->getWatchedItem(
1317 $this->getMockNonAnonUserWithId( 1 ),
1318 new TitleValue( 0, 'SomeDbKey' )
1319 );
1320 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
1321 $this->assertEquals( 1, $watchedItem->getUser()->getId() );
1322 $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
1323 $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
1324 }
1325
1326 public function testGetWatchedItem_cachedItem() {
1327 $mockDb = $this->getMockDb();
1328 $mockDb->expects( $this->never() )
1329 ->method( 'selectRow' );
1330
1331 $mockUser = $this->getMockNonAnonUserWithId( 1 );
1332 $linkTarget = new TitleValue( 0, 'SomeDbKey' );
1333 $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
1334
1335 $mockCache = $this->getMockCache();
1336 $mockCache->expects( $this->never() )->method( 'delete' );
1337 $mockCache->expects( $this->never() )->method( 'set' );
1338 $mockCache->expects( $this->once() )
1339 ->method( 'get' )
1340 ->with(
1341 '0:SomeDbKey:1'
1342 )
1343 ->will( $this->returnValue( $cachedItem ) );
1344
1345 $store = $this->newWatchedItemStore(
1346 $this->getMockLoadBalancer( $mockDb ),
1347 $mockCache,
1348 $this->getMockReadOnlyMode()
1349 );
1350
1351 $this->assertEquals(
1352 $cachedItem,
1353 $store->getWatchedItem(
1354 $mockUser,
1355 $linkTarget
1356 )
1357 );
1358 }
1359
1360 public function testGetWatchedItem_noItem() {
1361 $mockDb = $this->getMockDb();
1362 $mockDb->expects( $this->once() )
1363 ->method( 'selectRow' )
1364 ->with(
1365 'watchlist',
1366 'wl_notificationtimestamp',
1367 [
1368 'wl_user' => 1,
1369 'wl_namespace' => 0,
1370 'wl_title' => 'SomeDbKey',
1371 ]
1372 )
1373 ->will( $this->returnValue( [] ) );
1374
1375 $mockCache = $this->getMockCache();
1376 $mockCache->expects( $this->never() )->method( 'set' );
1377 $mockCache->expects( $this->never() )->method( 'delete' );
1378 $mockCache->expects( $this->once() )
1379 ->method( 'get' )
1380 ->with( '0:SomeDbKey:1' )
1381 ->will( $this->returnValue( false ) );
1382
1383 $store = $this->newWatchedItemStore(
1384 $this->getMockLoadBalancer( $mockDb ),
1385 $mockCache,
1386 $this->getMockReadOnlyMode()
1387 );
1388
1389 $this->assertFalse(
1390 $store->getWatchedItem(
1391 $this->getMockNonAnonUserWithId( 1 ),
1392 new TitleValue( 0, 'SomeDbKey' )
1393 )
1394 );
1395 }
1396
1397 public function testGetWatchedItem_anonymousUser() {
1398 $mockDb = $this->getMockDb();
1399 $mockDb->expects( $this->never() )
1400 ->method( 'selectRow' );
1401
1402 $mockCache = $this->getMockCache();
1403 $mockCache->expects( $this->never() )->method( 'set' );
1404 $mockCache->expects( $this->never() )->method( 'get' );
1405 $mockCache->expects( $this->never() )->method( 'delete' );
1406
1407 $store = $this->newWatchedItemStore(
1408 $this->getMockLoadBalancer( $mockDb ),
1409 $mockCache,
1410 $this->getMockReadOnlyMode()
1411 );
1412
1413 $this->assertFalse(
1414 $store->getWatchedItem(
1415 $this->getAnonUser(),
1416 new TitleValue( 0, 'SomeDbKey' )
1417 )
1418 );
1419 }
1420
1421 public function testGetWatchedItemsForUser() {
1422 $mockDb = $this->getMockDb();
1423 $mockDb->expects( $this->once() )
1424 ->method( 'select' )
1425 ->with(
1426 'watchlist',
1427 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1428 [ 'wl_user' => 1 ]
1429 )
1430 ->will( $this->returnValue( [
1431 $this->getFakeRow( [
1432 'wl_namespace' => 0,
1433 'wl_title' => 'Foo1',
1434 'wl_notificationtimestamp' => '20151212010101',
1435 ] ),
1436 $this->getFakeRow( [
1437 'wl_namespace' => 1,
1438 'wl_title' => 'Foo2',
1439 'wl_notificationtimestamp' => null,
1440 ] ),
1441 ] ) );
1442
1443 $mockCache = $this->getMockCache();
1444 $mockCache->expects( $this->never() )->method( 'delete' );
1445 $mockCache->expects( $this->never() )->method( 'get' );
1446 $mockCache->expects( $this->never() )->method( 'set' );
1447
1448 $store = $this->newWatchedItemStore(
1449 $this->getMockLoadBalancer( $mockDb ),
1450 $mockCache,
1451 $this->getMockReadOnlyMode()
1452 );
1453 $user = $this->getMockNonAnonUserWithId( 1 );
1454
1455 $watchedItems = $store->getWatchedItemsForUser( $user );
1456
1457 $this->assertInternalType( 'array', $watchedItems );
1458 $this->assertCount( 2, $watchedItems );
1459 foreach ( $watchedItems as $watchedItem ) {
1460 $this->assertInstanceOf( WatchedItem::class, $watchedItem );
1461 }
1462 $this->assertEquals(
1463 new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1464 $watchedItems[0]
1465 );
1466 $this->assertEquals(
1467 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1468 $watchedItems[1]
1469 );
1470 }
1471
1472 public function provideDbTypes() {
1473 return [
1474 [ false, DB_REPLICA ],
1475 [ true, DB_MASTER ],
1476 ];
1477 }
1478
1479 /**
1480 * @dataProvider provideDbTypes
1481 */
1482 public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) {
1483 $mockDb = $this->getMockDb();
1484 $mockCache = $this->getMockCache();
1485 $mockLoadBalancer = $this->getMockLoadBalancer( $mockDb, $dbType );
1486 $user = $this->getMockNonAnonUserWithId( 1 );
1487
1488 $mockDb->expects( $this->once() )
1489 ->method( 'select' )
1490 ->with(
1491 'watchlist',
1492 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1493 [ 'wl_user' => 1 ],
1494 $this->isType( 'string' ),
1495 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1496 )
1497 ->will( $this->returnValue( [] ) );
1498
1499 $store = $this->newWatchedItemStore(
1500 $mockLoadBalancer,
1501 $mockCache,
1502 $this->getMockReadOnlyMode()
1503 );
1504
1505 $watchedItems = $store->getWatchedItemsForUser(
1506 $user,
1507 [ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ]
1508 );
1509 $this->assertEquals( [], $watchedItems );
1510 }
1511
1512 public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
1513 $store = $this->newWatchedItemStore(
1514 $this->getMockLoadBalancer( $this->getMockDb() ),
1515 $this->getMockCache(),
1516 $this->getMockReadOnlyMode()
1517 );
1518
1519 $this->setExpectedException( InvalidArgumentException::class );
1520 $store->getWatchedItemsForUser(
1521 $this->getMockNonAnonUserWithId( 1 ),
1522 [ 'sort' => 'foo' ]
1523 );
1524 }
1525
1526 public function testIsWatchedItem_existingItem() {
1527 $mockDb = $this->getMockDb();
1528 $mockDb->expects( $this->once() )
1529 ->method( 'selectRow' )
1530 ->with(
1531 'watchlist',
1532 'wl_notificationtimestamp',
1533 [
1534 'wl_user' => 1,
1535 'wl_namespace' => 0,
1536 'wl_title' => 'SomeDbKey',
1537 ]
1538 )
1539 ->will( $this->returnValue(
1540 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
1541 ) );
1542
1543 $mockCache = $this->getMockCache();
1544 $mockCache->expects( $this->never() )->method( 'delete' );
1545 $mockCache->expects( $this->once() )
1546 ->method( 'get' )
1547 ->with( '0:SomeDbKey:1' )
1548 ->will( $this->returnValue( false ) );
1549 $mockCache->expects( $this->once() )
1550 ->method( 'set' )
1551 ->with(
1552 '0:SomeDbKey:1'
1553 );
1554
1555 $store = $this->newWatchedItemStore(
1556 $this->getMockLoadBalancer( $mockDb ),
1557 $mockCache,
1558 $this->getMockReadOnlyMode()
1559 );
1560
1561 $this->assertTrue(
1562 $store->isWatched(
1563 $this->getMockNonAnonUserWithId( 1 ),
1564 new TitleValue( 0, 'SomeDbKey' )
1565 )
1566 );
1567 }
1568
1569 public function testIsWatchedItem_noItem() {
1570 $mockDb = $this->getMockDb();
1571 $mockDb->expects( $this->once() )
1572 ->method( 'selectRow' )
1573 ->with(
1574 'watchlist',
1575 'wl_notificationtimestamp',
1576 [
1577 'wl_user' => 1,
1578 'wl_namespace' => 0,
1579 'wl_title' => 'SomeDbKey',
1580 ]
1581 )
1582 ->will( $this->returnValue( [] ) );
1583
1584 $mockCache = $this->getMockCache();
1585 $mockCache->expects( $this->never() )->method( 'set' );
1586 $mockCache->expects( $this->never() )->method( 'delete' );
1587 $mockCache->expects( $this->once() )
1588 ->method( 'get' )
1589 ->with( '0:SomeDbKey:1' )
1590 ->will( $this->returnValue( false ) );
1591
1592 $store = $this->newWatchedItemStore(
1593 $this->getMockLoadBalancer( $mockDb ),
1594 $mockCache,
1595 $this->getMockReadOnlyMode()
1596 );
1597
1598 $this->assertFalse(
1599 $store->isWatched(
1600 $this->getMockNonAnonUserWithId( 1 ),
1601 new TitleValue( 0, 'SomeDbKey' )
1602 )
1603 );
1604 }
1605
1606 public function testIsWatchedItem_anonymousUser() {
1607 $mockDb = $this->getMockDb();
1608 $mockDb->expects( $this->never() )
1609 ->method( 'selectRow' );
1610
1611 $mockCache = $this->getMockCache();
1612 $mockCache->expects( $this->never() )->method( 'set' );
1613 $mockCache->expects( $this->never() )->method( 'get' );
1614 $mockCache->expects( $this->never() )->method( 'delete' );
1615
1616 $store = $this->newWatchedItemStore(
1617 $this->getMockLoadBalancer( $mockDb ),
1618 $mockCache,
1619 $this->getMockReadOnlyMode()
1620 );
1621
1622 $this->assertFalse(
1623 $store->isWatched(
1624 $this->getAnonUser(),
1625 new TitleValue( 0, 'SomeDbKey' )
1626 )
1627 );
1628 }
1629
1630 public function testGetNotificationTimestampsBatch() {
1631 $targets = [
1632 new TitleValue( 0, 'SomeDbKey' ),
1633 new TitleValue( 1, 'AnotherDbKey' ),
1634 ];
1635
1636 $mockDb = $this->getMockDb();
1637 $dbResult = [
1638 $this->getFakeRow( [
1639 'wl_namespace' => '0',
1640 'wl_title' => 'SomeDbKey',
1641 'wl_notificationtimestamp' => '20151212010101',
1642 ] ),
1643 $this->getFakeRow(
1644 [
1645 'wl_namespace' => '1',
1646 'wl_title' => 'AnotherDbKey',
1647 'wl_notificationtimestamp' => null,
1648 ]
1649 ),
1650 ];
1651
1652 $mockDb->expects( $this->once() )
1653 ->method( 'makeWhereFrom2d' )
1654 ->with(
1655 [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
1656 $this->isType( 'string' ),
1657 $this->isType( 'string' )
1658 )
1659 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
1660 $mockDb->expects( $this->once() )
1661 ->method( 'select' )
1662 ->with(
1663 'watchlist',
1664 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1665 [
1666 'makeWhereFrom2d return value',
1667 'wl_user' => 1
1668 ],
1669 $this->isType( 'string' )
1670 )
1671 ->will( $this->returnValue( $dbResult ) );
1672
1673 $mockCache = $this->getMockCache();
1674 $mockCache->expects( $this->exactly( 2 ) )
1675 ->method( 'get' )
1676 ->withConsecutive(
1677 [ '0:SomeDbKey:1' ],
1678 [ '1:AnotherDbKey:1' ]
1679 )
1680 ->will( $this->returnValue( null ) );
1681 $mockCache->expects( $this->never() )->method( 'set' );
1682 $mockCache->expects( $this->never() )->method( 'delete' );
1683
1684 $store = $this->newWatchedItemStore(
1685 $this->getMockLoadBalancer( $mockDb ),
1686 $mockCache,
1687 $this->getMockReadOnlyMode()
1688 );
1689
1690 $this->assertEquals(
1691 [
1692 0 => [ 'SomeDbKey' => '20151212010101', ],
1693 1 => [ 'AnotherDbKey' => null, ],
1694 ],
1695 $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
1696 );
1697 }
1698
1699 public function testGetNotificationTimestampsBatch_notWatchedTarget() {
1700 $targets = [
1701 new TitleValue( 0, 'OtherDbKey' ),
1702 ];
1703
1704 $mockDb = $this->getMockDb();
1705
1706 $mockDb->expects( $this->once() )
1707 ->method( 'makeWhereFrom2d' )
1708 ->with(
1709 [ [ 'OtherDbKey' => 1 ] ],
1710 $this->isType( 'string' ),
1711 $this->isType( 'string' )
1712 )
1713 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
1714 $mockDb->expects( $this->once() )
1715 ->method( 'select' )
1716 ->with(
1717 'watchlist',
1718 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1719 [
1720 'makeWhereFrom2d return value',
1721 'wl_user' => 1
1722 ],
1723 $this->isType( 'string' )
1724 )
1725 ->will( $this->returnValue( $this->getFakeRow( [] ) ) );
1726
1727 $mockCache = $this->getMockCache();
1728 $mockCache->expects( $this->once() )
1729 ->method( 'get' )
1730 ->with( '0:OtherDbKey:1' )
1731 ->will( $this->returnValue( null ) );
1732 $mockCache->expects( $this->never() )->method( 'set' );
1733 $mockCache->expects( $this->never() )->method( 'delete' );
1734
1735 $store = $this->newWatchedItemStore(
1736 $this->getMockLoadBalancer( $mockDb ),
1737 $mockCache,
1738 $this->getMockReadOnlyMode()
1739 );
1740
1741 $this->assertEquals(
1742 [
1743 0 => [ 'OtherDbKey' => false, ],
1744 ],
1745 $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
1746 );
1747 }
1748
1749 public function testGetNotificationTimestampsBatch_cachedItem() {
1750 $targets = [
1751 new TitleValue( 0, 'SomeDbKey' ),
1752 new TitleValue( 1, 'AnotherDbKey' ),
1753 ];
1754
1755 $user = $this->getMockNonAnonUserWithId( 1 );
1756 $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
1757
1758 $mockDb = $this->getMockDb();
1759
1760 $mockDb->expects( $this->once() )
1761 ->method( 'makeWhereFrom2d' )
1762 ->with(
1763 [ 1 => [ 'AnotherDbKey' => 1 ] ],
1764 $this->isType( 'string' ),
1765 $this->isType( 'string' )
1766 )
1767 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
1768 $mockDb->expects( $this->once() )
1769 ->method( 'select' )
1770 ->with(
1771 'watchlist',
1772 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1773 [
1774 'makeWhereFrom2d return value',
1775 'wl_user' => 1
1776 ],
1777 $this->isType( 'string' )
1778 )
1779 ->will( $this->returnValue( [
1780 $this->getFakeRow(
1781 [ 'wl_namespace' => '1', 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ]
1782 )
1783 ] ) );
1784
1785 $mockCache = $this->getMockCache();
1786 $mockCache->expects( $this->at( 1 ) )
1787 ->method( 'get' )
1788 ->with( '0:SomeDbKey:1' )
1789 ->will( $this->returnValue( $cachedItem ) );
1790 $mockCache->expects( $this->at( 3 ) )
1791 ->method( 'get' )
1792 ->with( '1:AnotherDbKey:1' )
1793 ->will( $this->returnValue( null ) );
1794 $mockCache->expects( $this->never() )->method( 'set' );
1795 $mockCache->expects( $this->never() )->method( 'delete' );
1796
1797 $store = $this->newWatchedItemStore(
1798 $this->getMockLoadBalancer( $mockDb ),
1799 $mockCache,
1800 $this->getMockReadOnlyMode()
1801 );
1802
1803 $this->assertEquals(
1804 [
1805 0 => [ 'SomeDbKey' => '20151212010101', ],
1806 1 => [ 'AnotherDbKey' => null, ],
1807 ],
1808 $store->getNotificationTimestampsBatch( $user, $targets )
1809 );
1810 }
1811
1812 public function testGetNotificationTimestampsBatch_allItemsCached() {
1813 $targets = [
1814 new TitleValue( 0, 'SomeDbKey' ),
1815 new TitleValue( 1, 'AnotherDbKey' ),
1816 ];
1817
1818 $user = $this->getMockNonAnonUserWithId( 1 );
1819 $cachedItems = [
1820 new WatchedItem( $user, $targets[0], '20151212010101' ),
1821 new WatchedItem( $user, $targets[1], null ),
1822 ];
1823 $mockDb = $this->getMockDb();
1824 $mockDb->expects( $this->never() )->method( $this->anything() );
1825
1826 $mockCache = $this->getMockCache();
1827 $mockCache->expects( $this->at( 1 ) )
1828 ->method( 'get' )
1829 ->with( '0:SomeDbKey:1' )
1830 ->will( $this->returnValue( $cachedItems[0] ) );
1831 $mockCache->expects( $this->at( 3 ) )
1832 ->method( 'get' )
1833 ->with( '1:AnotherDbKey:1' )
1834 ->will( $this->returnValue( $cachedItems[1] ) );
1835 $mockCache->expects( $this->never() )->method( 'set' );
1836 $mockCache->expects( $this->never() )->method( 'delete' );
1837
1838 $store = $this->newWatchedItemStore(
1839 $this->getMockLoadBalancer( $mockDb ),
1840 $mockCache,
1841 $this->getMockReadOnlyMode()
1842 );
1843
1844 $this->assertEquals(
1845 [
1846 0 => [ 'SomeDbKey' => '20151212010101', ],
1847 1 => [ 'AnotherDbKey' => null, ],
1848 ],
1849 $store->getNotificationTimestampsBatch( $user, $targets )
1850 );
1851 }
1852
1853 public function testGetNotificationTimestampsBatch_anonymousUser() {
1854 $targets = [
1855 new TitleValue( 0, 'SomeDbKey' ),
1856 new TitleValue( 1, 'AnotherDbKey' ),
1857 ];
1858
1859 $mockDb = $this->getMockDb();
1860 $mockDb->expects( $this->never() )->method( $this->anything() );
1861
1862 $mockCache = $this->getMockCache();
1863 $mockCache->expects( $this->never() )->method( $this->anything() );
1864
1865 $store = $this->newWatchedItemStore(
1866 $this->getMockLoadBalancer( $mockDb ),
1867 $mockCache,
1868 $this->getMockReadOnlyMode()
1869 );
1870
1871 $this->assertEquals(
1872 [
1873 0 => [ 'SomeDbKey' => false, ],
1874 1 => [ 'AnotherDbKey' => false, ],
1875 ],
1876 $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets )
1877 );
1878 }
1879
1880 public function testResetNotificationTimestamp_anonymousUser() {
1881 $mockDb = $this->getMockDb();
1882 $mockDb->expects( $this->never() )
1883 ->method( 'selectRow' );
1884
1885 $mockCache = $this->getMockCache();
1886 $mockCache->expects( $this->never() )->method( 'get' );
1887 $mockCache->expects( $this->never() )->method( 'set' );
1888 $mockCache->expects( $this->never() )->method( 'delete' );
1889
1890 $store = $this->newWatchedItemStore(
1891 $this->getMockLoadBalancer( $mockDb ),
1892 $mockCache,
1893 $this->getMockReadOnlyMode()
1894 );
1895
1896 $this->assertFalse(
1897 $store->resetNotificationTimestamp(
1898 $this->getAnonUser(),
1899 Title::newFromText( 'SomeDbKey' )
1900 )
1901 );
1902 }
1903
1904 public function testResetNotificationTimestamp_noItem() {
1905 $mockDb = $this->getMockDb();
1906 $mockDb->expects( $this->once() )
1907 ->method( 'selectRow' )
1908 ->with(
1909 'watchlist',
1910 'wl_notificationtimestamp',
1911 [
1912 'wl_user' => 1,
1913 'wl_namespace' => 0,
1914 'wl_title' => 'SomeDbKey',
1915 ]
1916 )
1917 ->will( $this->returnValue( [] ) );
1918
1919 $mockCache = $this->getMockCache();
1920 $mockCache->expects( $this->never() )->method( 'get' );
1921 $mockCache->expects( $this->never() )->method( 'set' );
1922 $mockCache->expects( $this->never() )->method( 'delete' );
1923
1924 $store = $this->newWatchedItemStore(
1925 $this->getMockLoadBalancer( $mockDb ),
1926 $mockCache,
1927 $this->getMockReadOnlyMode()
1928 );
1929
1930 $this->assertFalse(
1931 $store->resetNotificationTimestamp(
1932 $this->getMockNonAnonUserWithId( 1 ),
1933 Title::newFromText( 'SomeDbKey' )
1934 )
1935 );
1936 }
1937
1938 public function testResetNotificationTimestamp_item() {
1939 $user = $this->getMockNonAnonUserWithId( 1 );
1940 $title = Title::newFromText( 'SomeDbKey' );
1941
1942 $mockDb = $this->getMockDb();
1943 $mockDb->expects( $this->once() )
1944 ->method( 'selectRow' )
1945 ->with(
1946 'watchlist',
1947 'wl_notificationtimestamp',
1948 [
1949 'wl_user' => 1,
1950 'wl_namespace' => 0,
1951 'wl_title' => 'SomeDbKey',
1952 ]
1953 )
1954 ->will( $this->returnValue(
1955 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
1956 ) );
1957
1958 $mockCache = $this->getMockCache();
1959 $mockCache->expects( $this->never() )->method( 'get' );
1960 $mockCache->expects( $this->once() )
1961 ->method( 'set' )
1962 ->with(
1963 '0:SomeDbKey:1',
1964 $this->isInstanceOf( WatchedItem::class )
1965 );
1966 $mockCache->expects( $this->once() )
1967 ->method( 'delete' )
1968 ->with( '0:SomeDbKey:1' );
1969
1970 $store = $this->newWatchedItemStore(
1971 $this->getMockLoadBalancer( $mockDb ),
1972 $mockCache,
1973 $this->getMockReadOnlyMode()
1974 );
1975
1976 // Note: This does not actually assert the job is correct
1977 $callableCallCounter = 0;
1978 $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
1979 $callableCallCounter++;
1980 $this->assertInternalType( 'callable', $callable );
1981 };
1982 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
1983
1984 $this->assertTrue(
1985 $store->resetNotificationTimestamp(
1986 $user,
1987 $title
1988 )
1989 );
1990 $this->assertEquals( 1, $callableCallCounter );
1991
1992 ScopedCallback::consume( $scopedOverride );
1993 }
1994
1995 public function testResetNotificationTimestamp_noItemForced() {
1996 $user = $this->getMockNonAnonUserWithId( 1 );
1997 $title = Title::newFromText( 'SomeDbKey' );
1998
1999 $mockDb = $this->getMockDb();
2000 $mockDb->expects( $this->never() )
2001 ->method( 'selectRow' );
2002
2003 $mockCache = $this->getMockCache();
2004 $mockDb->expects( $this->never() )
2005 ->method( 'get' );
2006 $mockDb->expects( $this->never() )
2007 ->method( 'set' );
2008 $mockDb->expects( $this->never() )
2009 ->method( 'delete' );
2010
2011 $store = $this->newWatchedItemStore(
2012 $this->getMockLoadBalancer( $mockDb ),
2013 $mockCache,
2014 $this->getMockReadOnlyMode()
2015 );
2016
2017 // Note: This does not actually assert the job is correct
2018 $callableCallCounter = 0;
2019 $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
2020 $callableCallCounter++;
2021 $this->assertInternalType( 'callable', $callable );
2022 };
2023 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
2024
2025 $this->assertTrue(
2026 $store->resetNotificationTimestamp(
2027 $user,
2028 $title,
2029 'force'
2030 )
2031 );
2032 $this->assertEquals( 1, $callableCallCounter );
2033
2034 ScopedCallback::consume( $scopedOverride );
2035 }
2036
2037 /**
2038 * @param string $text
2039 * @param int $ns
2040 *
2041 * @return PHPUnit_Framework_MockObject_MockObject|Title
2042 */
2043 private function getMockTitle( $text, $ns = 0 ) {
2044 $title = $this->createMock( Title::class );
2045 $title->expects( $this->any() )
2046 ->method( 'getText' )
2047 ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
2048 $title->expects( $this->any() )
2049 ->method( 'getDbKey' )
2050 ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
2051 $title->expects( $this->any() )
2052 ->method( 'getNamespace' )
2053 ->will( $this->returnValue( $ns ) );
2054 return $title;
2055 }
2056
2057 private function verifyCallbackJob(
2058 $callback,
2059 LinkTarget $expectedTitle,
2060 $expectedUserId,
2061 callable $notificationTimestampCondition
2062 ) {
2063 $this->assertInternalType( 'callable', $callback );
2064
2065 $callbackReflector = new ReflectionFunction( $callback );
2066 $vars = $callbackReflector->getStaticVariables();
2067 $this->assertArrayHasKey( 'job', $vars );
2068 $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] );
2069
2070 /** @var ActivityUpdateJob $job */
2071 $job = $vars['job'];
2072 $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() );
2073 $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() );
2074
2075 $jobParams = $job->getParams();
2076 $this->assertArrayHasKey( 'type', $jobParams );
2077 $this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] );
2078 $this->assertArrayHasKey( 'userid', $jobParams );
2079 $this->assertEquals( $expectedUserId, $jobParams['userid'] );
2080 $this->assertArrayHasKey( 'notifTime', $jobParams );
2081 $this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) );
2082 }
2083
2084 public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
2085 $user = $this->getMockNonAnonUserWithId( 1 );
2086 $oldid = 22;
2087 $title = $this->getMockTitle( 'SomeTitle' );
2088 $title->expects( $this->once() )
2089 ->method( 'getNextRevisionID' )
2090 ->with( $oldid )
2091 ->will( $this->returnValue( false ) );
2092
2093 $mockDb = $this->getMockDb();
2094 $mockDb->expects( $this->never() )
2095 ->method( 'selectRow' );
2096
2097 $mockCache = $this->getMockCache();
2098 $mockDb->expects( $this->never() )
2099 ->method( 'get' );
2100 $mockDb->expects( $this->never() )
2101 ->method( 'set' );
2102 $mockDb->expects( $this->never() )
2103 ->method( 'delete' );
2104
2105 $store = $this->newWatchedItemStore(
2106 $this->getMockLoadBalancer( $mockDb ),
2107 $mockCache,
2108 $this->getMockReadOnlyMode()
2109 );
2110
2111 $callableCallCounter = 0;
2112 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
2113 function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
2114 $callableCallCounter++;
2115 $this->verifyCallbackJob(
2116 $callable,
2117 $title,
2118 $user->getId(),
2119 function ( $time ) {
2120 return $time === null;
2121 }
2122 );
2123 }
2124 );
2125
2126 $this->assertTrue(
2127 $store->resetNotificationTimestamp(
2128 $user,
2129 $title,
2130 'force',
2131 $oldid
2132 )
2133 );
2134 $this->assertEquals( 1, $callableCallCounter );
2135
2136 ScopedCallback::consume( $scopedOverride );
2137 }
2138
2139 public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
2140 $user = $this->getMockNonAnonUserWithId( 1 );
2141 $oldid = 22;
2142 $title = $this->getMockTitle( 'SomeDbKey' );
2143 $title->expects( $this->once() )
2144 ->method( 'getNextRevisionID' )
2145 ->with( $oldid )
2146 ->will( $this->returnValue( 33 ) );
2147
2148 $mockDb = $this->getMockDb();
2149 $mockDb->expects( $this->once() )
2150 ->method( 'selectRow' )
2151 ->with(
2152 'watchlist',
2153 'wl_notificationtimestamp',
2154 [
2155 'wl_user' => 1,
2156 'wl_namespace' => 0,
2157 'wl_title' => 'SomeDbKey',
2158 ]
2159 )
2160 ->will( $this->returnValue(
2161 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
2162 ) );
2163
2164 $mockCache = $this->getMockCache();
2165 $mockDb->expects( $this->never() )
2166 ->method( 'get' );
2167 $mockDb->expects( $this->never() )
2168 ->method( 'set' );
2169 $mockDb->expects( $this->never() )
2170 ->method( 'delete' );
2171
2172 $store = $this->newWatchedItemStore(
2173 $this->getMockLoadBalancer( $mockDb ),
2174 $mockCache,
2175 $this->getMockReadOnlyMode()
2176 );
2177
2178 $addUpdateCallCounter = 0;
2179 $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
2180 function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
2181 $addUpdateCallCounter++;
2182 $this->verifyCallbackJob(
2183 $callable,
2184 $title,
2185 $user->getId(),
2186 function ( $time ) {
2187 return $time !== null && $time > '20151212010101';
2188 }
2189 );
2190 }
2191 );
2192
2193 $getTimestampCallCounter = 0;
2194 $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
2195 function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
2196 $getTimestampCallCounter++;
2197 $this->assertEquals( $title, $titleParam );
2198 $this->assertEquals( $oldid, $oldidParam );
2199 }
2200 );
2201
2202 $this->assertTrue(
2203 $store->resetNotificationTimestamp(
2204 $user,
2205 $title,
2206 'force',
2207 $oldid
2208 )
2209 );
2210 $this->assertEquals( 1, $addUpdateCallCounter );
2211 $this->assertEquals( 1, $getTimestampCallCounter );
2212
2213 ScopedCallback::consume( $scopedOverrideDeferred );
2214 ScopedCallback::consume( $scopedOverrideRevision );
2215 }
2216
2217 public function testResetNotificationTimestamp_notWatchedPageForced() {
2218 $user = $this->getMockNonAnonUserWithId( 1 );
2219 $oldid = 22;
2220 $title = $this->getMockTitle( 'SomeDbKey' );
2221 $title->expects( $this->once() )
2222 ->method( 'getNextRevisionID' )
2223 ->with( $oldid )
2224 ->will( $this->returnValue( 33 ) );
2225
2226 $mockDb = $this->getMockDb();
2227 $mockDb->expects( $this->once() )
2228 ->method( 'selectRow' )
2229 ->with(
2230 'watchlist',
2231 'wl_notificationtimestamp',
2232 [
2233 'wl_user' => 1,
2234 'wl_namespace' => 0,
2235 'wl_title' => 'SomeDbKey',
2236 ]
2237 )
2238 ->will( $this->returnValue( false ) );
2239
2240 $mockCache = $this->getMockCache();
2241 $mockDb->expects( $this->never() )
2242 ->method( 'get' );
2243 $mockDb->expects( $this->never() )
2244 ->method( 'set' );
2245 $mockDb->expects( $this->never() )
2246 ->method( 'delete' );
2247
2248 $store = $this->newWatchedItemStore(
2249 $this->getMockLoadBalancer( $mockDb ),
2250 $mockCache,
2251 $this->getMockReadOnlyMode()
2252 );
2253
2254 $callableCallCounter = 0;
2255 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
2256 function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
2257 $callableCallCounter++;
2258 $this->verifyCallbackJob(
2259 $callable,
2260 $title,
2261 $user->getId(),
2262 function ( $time ) {
2263 return $time === null;
2264 }
2265 );
2266 }
2267 );
2268
2269 $this->assertTrue(
2270 $store->resetNotificationTimestamp(
2271 $user,
2272 $title,
2273 'force',
2274 $oldid
2275 )
2276 );
2277 $this->assertEquals( 1, $callableCallCounter );
2278
2279 ScopedCallback::consume( $scopedOverride );
2280 }
2281
2282 public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
2283 $user = $this->getMockNonAnonUserWithId( 1 );
2284 $oldid = 22;
2285 $title = $this->getMockTitle( 'SomeDbKey' );
2286 $title->expects( $this->once() )
2287 ->method( 'getNextRevisionID' )
2288 ->with( $oldid )
2289 ->will( $this->returnValue( 33 ) );
2290
2291 $mockDb = $this->getMockDb();
2292 $mockDb->expects( $this->once() )
2293 ->method( 'selectRow' )
2294 ->with(
2295 'watchlist',
2296 'wl_notificationtimestamp',
2297 [
2298 'wl_user' => 1,
2299 'wl_namespace' => 0,
2300 'wl_title' => 'SomeDbKey',
2301 ]
2302 )
2303 ->will( $this->returnValue(
2304 $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
2305 ) );
2306
2307 $mockCache = $this->getMockCache();
2308 $mockDb->expects( $this->never() )
2309 ->method( 'get' );
2310 $mockDb->expects( $this->never() )
2311 ->method( 'set' );
2312 $mockDb->expects( $this->never() )
2313 ->method( 'delete' );
2314
2315 $store = $this->newWatchedItemStore(
2316 $this->getMockLoadBalancer( $mockDb ),
2317 $mockCache,
2318 $this->getMockReadOnlyMode()
2319 );
2320
2321 $addUpdateCallCounter = 0;
2322 $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
2323 function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
2324 $addUpdateCallCounter++;
2325 $this->verifyCallbackJob(
2326 $callable,
2327 $title,
2328 $user->getId(),
2329 function ( $time ) {
2330 return $time === '30151212010101';
2331 }
2332 );
2333 }
2334 );
2335
2336 $getTimestampCallCounter = 0;
2337 $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
2338 function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
2339 $getTimestampCallCounter++;
2340 $this->assertEquals( $title, $titleParam );
2341 $this->assertEquals( $oldid, $oldidParam );
2342 }
2343 );
2344
2345 $this->assertTrue(
2346 $store->resetNotificationTimestamp(
2347 $user,
2348 $title,
2349 'force',
2350 $oldid
2351 )
2352 );
2353 $this->assertEquals( 1, $addUpdateCallCounter );
2354 $this->assertEquals( 1, $getTimestampCallCounter );
2355
2356 ScopedCallback::consume( $scopedOverrideDeferred );
2357 ScopedCallback::consume( $scopedOverrideRevision );
2358 }
2359
2360 public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() {
2361 $user = $this->getMockNonAnonUserWithId( 1 );
2362 $oldid = 22;
2363 $title = $this->getMockTitle( 'SomeDbKey' );
2364 $title->expects( $this->once() )
2365 ->method( 'getNextRevisionID' )
2366 ->with( $oldid )
2367 ->will( $this->returnValue( 33 ) );
2368
2369 $mockDb = $this->getMockDb();
2370 $mockDb->expects( $this->once() )
2371 ->method( 'selectRow' )
2372 ->with(
2373 'watchlist',
2374 'wl_notificationtimestamp',
2375 [
2376 'wl_user' => 1,
2377 'wl_namespace' => 0,
2378 'wl_title' => 'SomeDbKey',
2379 ]
2380 )
2381 ->will( $this->returnValue(
2382 $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
2383 ) );
2384
2385 $mockCache = $this->getMockCache();
2386 $mockDb->expects( $this->never() )
2387 ->method( 'get' );
2388 $mockDb->expects( $this->never() )
2389 ->method( 'set' );
2390 $mockDb->expects( $this->never() )
2391 ->method( 'delete' );
2392
2393 $store = $this->newWatchedItemStore(
2394 $this->getMockLoadBalancer( $mockDb ),
2395 $mockCache,
2396 $this->getMockReadOnlyMode()
2397 );
2398
2399 $addUpdateCallCounter = 0;
2400 $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
2401 function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
2402 $addUpdateCallCounter++;
2403 $this->verifyCallbackJob(
2404 $callable,
2405 $title,
2406 $user->getId(),
2407 function ( $time ) {
2408 return $time === false;
2409 }
2410 );
2411 }
2412 );
2413
2414 $getTimestampCallCounter = 0;
2415 $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
2416 function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
2417 $getTimestampCallCounter++;
2418 $this->assertEquals( $title, $titleParam );
2419 $this->assertEquals( $oldid, $oldidParam );
2420 }
2421 );
2422
2423 $this->assertTrue(
2424 $store->resetNotificationTimestamp(
2425 $user,
2426 $title,
2427 '',
2428 $oldid
2429 )
2430 );
2431 $this->assertEquals( 1, $addUpdateCallCounter );
2432 $this->assertEquals( 1, $getTimestampCallCounter );
2433
2434 ScopedCallback::consume( $scopedOverrideDeferred );
2435 ScopedCallback::consume( $scopedOverrideRevision );
2436 }
2437
2438 public function testSetNotificationTimestampsForUser_anonUser() {
2439 $store = $this->newWatchedItemStore(
2440 $this->getMockLoadBalancer( $this->getMockDb() ),
2441 $this->getMockCache(),
2442 $this->getMockReadOnlyMode()
2443 );
2444 $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) );
2445 }
2446
2447 public function testSetNotificationTimestampsForUser_allRows() {
2448 $user = $this->getMockNonAnonUserWithId( 1 );
2449 $timestamp = '20100101010101';
2450
2451 $mockDb = $this->getMockDb();
2452 $mockDb->expects( $this->once() )
2453 ->method( 'update' )
2454 ->with(
2455 'watchlist',
2456 [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
2457 [ 'wl_user' => 1 ]
2458 )
2459 ->will( $this->returnValue( true ) );
2460 $mockDb->expects( $this->exactly( 1 ) )
2461 ->method( 'timestamp' )
2462 ->will( $this->returnCallback( function ( $value ) {
2463 return 'TS' . $value . 'TS';
2464 } ) );
2465
2466 $store = $this->newWatchedItemStore(
2467 $this->getMockLoadBalancer( $mockDb ),
2468 $this->getMockCache(),
2469 $this->getMockReadOnlyMode()
2470 );
2471
2472 $this->assertTrue(
2473 $store->setNotificationTimestampsForUser( $user, $timestamp )
2474 );
2475 }
2476
2477 public function testSetNotificationTimestampsForUser_nullTimestamp() {
2478 $user = $this->getMockNonAnonUserWithId( 1 );
2479 $timestamp = null;
2480
2481 $mockDb = $this->getMockDb();
2482 $mockDb->expects( $this->once() )
2483 ->method( 'update' )
2484 ->with(
2485 'watchlist',
2486 [ 'wl_notificationtimestamp' => null ],
2487 [ 'wl_user' => 1 ]
2488 )
2489 ->will( $this->returnValue( true ) );
2490 $mockDb->expects( $this->exactly( 0 ) )
2491 ->method( 'timestamp' )
2492 ->will( $this->returnCallback( function ( $value ) {
2493 return 'TS' . $value . 'TS';
2494 } ) );
2495
2496 $store = $this->newWatchedItemStore(
2497 $this->getMockLoadBalancer( $mockDb ),
2498 $this->getMockCache(),
2499 $this->getMockReadOnlyMode()
2500 );
2501
2502 $this->assertTrue(
2503 $store->setNotificationTimestampsForUser( $user, $timestamp )
2504 );
2505 }
2506
2507 public function testSetNotificationTimestampsForUser_specificTargets() {
2508 $user = $this->getMockNonAnonUserWithId( 1 );
2509 $timestamp = '20100101010101';
2510 $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ];
2511
2512 $mockDb = $this->getMockDb();
2513 $mockDb->expects( $this->once() )
2514 ->method( 'update' )
2515 ->with(
2516 'watchlist',
2517 [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
2518 [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
2519 )
2520 ->will( $this->returnValue( true ) );
2521 $mockDb->expects( $this->exactly( 1 ) )
2522 ->method( 'timestamp' )
2523 ->will( $this->returnCallback( function ( $value ) {
2524 return 'TS' . $value . 'TS';
2525 } ) );
2526 $mockDb->expects( $this->once() )
2527 ->method( 'makeWhereFrom2d' )
2528 ->with(
2529 [ [ 'Foo' => 1, 'Bar' => 1 ] ],
2530 $this->isType( 'string' ),
2531 $this->isType( 'string' )
2532 )
2533 ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
2534
2535 $store = $this->newWatchedItemStore(
2536 $this->getMockLoadBalancer( $mockDb ),
2537 $this->getMockCache(),
2538 $this->getMockReadOnlyMode()
2539 );
2540
2541 $this->assertTrue(
2542 $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
2543 );
2544 }
2545
2546 public function testUpdateNotificationTimestamp_watchersExist() {
2547 $mockDb = $this->getMockDb();
2548 $mockDb->expects( $this->once() )
2549 ->method( 'selectFieldValues' )
2550 ->with(
2551 'watchlist',
2552 'wl_user',
2553 [
2554 'wl_user != 1',
2555 'wl_namespace' => 0,
2556 'wl_title' => 'SomeDbKey',
2557 'wl_notificationtimestamp IS NULL'
2558 ]
2559 )
2560 ->will( $this->returnValue( [ '2', '3' ] ) );
2561 $mockDb->expects( $this->once() )
2562 ->method( 'update' )
2563 ->with(
2564 'watchlist',
2565 [ 'wl_notificationtimestamp' => null ],
2566 [
2567 'wl_user' => [ 2, 3 ],
2568 'wl_namespace' => 0,
2569 'wl_title' => 'SomeDbKey',
2570 ]
2571 );
2572
2573 $mockCache = $this->getMockCache();
2574 $mockCache->expects( $this->never() )->method( 'set' );
2575 $mockCache->expects( $this->never() )->method( 'get' );
2576 $mockCache->expects( $this->never() )->method( 'delete' );
2577
2578 $store = $this->newWatchedItemStore(
2579 $this->getMockLoadBalancer( $mockDb ),
2580 $mockCache,
2581 $this->getMockReadOnlyMode()
2582 );
2583
2584 $this->assertEquals(
2585 [ 2, 3 ],
2586 $store->updateNotificationTimestamp(
2587 $this->getMockNonAnonUserWithId( 1 ),
2588 new TitleValue( 0, 'SomeDbKey' ),
2589 '20151212010101'
2590 )
2591 );
2592 }
2593
2594 public function testUpdateNotificationTimestamp_noWatchers() {
2595 $mockDb = $this->getMockDb();
2596 $mockDb->expects( $this->once() )
2597 ->method( 'selectFieldValues' )
2598 ->with(
2599 'watchlist',
2600 'wl_user',
2601 [
2602 'wl_user != 1',
2603 'wl_namespace' => 0,
2604 'wl_title' => 'SomeDbKey',
2605 'wl_notificationtimestamp IS NULL'
2606 ]
2607 )
2608 ->will(
2609 $this->returnValue( [] )
2610 );
2611 $mockDb->expects( $this->never() )
2612 ->method( 'update' );
2613
2614 $mockCache = $this->getMockCache();
2615 $mockCache->expects( $this->never() )->method( 'set' );
2616 $mockCache->expects( $this->never() )->method( 'get' );
2617 $mockCache->expects( $this->never() )->method( 'delete' );
2618
2619 $store = $this->newWatchedItemStore(
2620 $this->getMockLoadBalancer( $mockDb ),
2621 $mockCache,
2622 $this->getMockReadOnlyMode()
2623 );
2624
2625 $watchers = $store->updateNotificationTimestamp(
2626 $this->getMockNonAnonUserWithId( 1 ),
2627 new TitleValue( 0, 'SomeDbKey' ),
2628 '20151212010101'
2629 );
2630 $this->assertInternalType( 'array', $watchers );
2631 $this->assertEmpty( $watchers );
2632 }
2633
2634 public function testUpdateNotificationTimestamp_clearsCachedItems() {
2635 $user = $this->getMockNonAnonUserWithId( 1 );
2636 $titleValue = new TitleValue( 0, 'SomeDbKey' );
2637
2638 $mockDb = $this->getMockDb();
2639 $mockDb->expects( $this->once() )
2640 ->method( 'selectRow' )
2641 ->will( $this->returnValue(
2642 $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
2643 ) );
2644 $mockDb->expects( $this->once() )
2645 ->method( 'selectFieldValues' )
2646 ->will(
2647 $this->returnValue( [ '2', '3' ] )
2648 );
2649 $mockDb->expects( $this->once() )
2650 ->method( 'update' );
2651
2652 $mockCache = $this->getMockCache();
2653 $mockCache->expects( $this->once() )
2654 ->method( 'set' )
2655 ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
2656 $mockCache->expects( $this->once() )
2657 ->method( 'get' )
2658 ->with( '0:SomeDbKey:1' );
2659 $mockCache->expects( $this->once() )
2660 ->method( 'delete' )
2661 ->with( '0:SomeDbKey:1' );
2662
2663 $store = $this->newWatchedItemStore(
2664 $this->getMockLoadBalancer( $mockDb ),
2665 $mockCache,
2666 $this->getMockReadOnlyMode()
2667 );
2668
2669 // This will add the item to the cache
2670 $store->getWatchedItem( $user, $titleValue );
2671
2672 $store->updateNotificationTimestamp(
2673 $this->getMockNonAnonUserWithId( 1 ),
2674 $titleValue,
2675 '20151212010101'
2676 );
2677 }
2678
2679 }