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