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