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