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