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