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