Skin: Make skins aware of their registered skin name
[lhc/web/wiklou.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 }