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