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