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