1<?php 2 3use MediaWiki\Cache\LinkBatchFactory; 4use MediaWiki\Config\ServiceOptions; 5use MediaWiki\Linker\LinkTarget; 6use MediaWiki\Page\PageIdentityValue; 7use MediaWiki\Revision\RevisionLookup; 8use MediaWiki\Revision\RevisionRecord; 9use MediaWiki\Tests\Unit\DummyServicesTrait; 10use MediaWiki\User\UserFactory; 11use MediaWiki\User\UserIdentity; 12use MediaWiki\User\UserIdentityValue; 13use PHPUnit\Framework\MockObject\MockObject; 14use Wikimedia\Rdbms\IDatabase; 15use Wikimedia\Rdbms\LBFactory; 16use Wikimedia\Rdbms\LoadBalancer; 17use Wikimedia\TestingAccessWrapper; 18 19/** 20 * @author Addshore 21 * @author DannyS712 22 * 23 * @covers WatchedItemStore 24 */ 25class WatchedItemStoreUnitTest extends MediaWikiUnitTestCase { 26 use DummyServicesTrait; 27 use MockTitleTrait; 28 29 /** 30 * @return MockObject|IDatabase 31 */ 32 private function getMockDb() { 33 return $this->createMock( IDatabase::class ); 34 } 35 36 /** 37 * @param IDatabase $mockDb 38 * @param string|null $expectedConnectionType 39 * @return MockObject|LoadBalancer 40 */ 41 private function getMockLoadBalancer( 42 $mockDb, 43 $expectedConnectionType = null 44 ) { 45 $mock = $this->createMock( LoadBalancer::class ); 46 if ( $expectedConnectionType !== null ) { 47 $mock->method( 'getConnectionRef' ) 48 ->with( $expectedConnectionType ) 49 ->willReturn( $mockDb ); 50 } else { 51 $mock->method( 'getConnectionRef' ) 52 ->willReturn( $mockDb ); 53 } 54 return $mock; 55 } 56 57 /** 58 * @param IDatabase $mockDb 59 * @param string|null $expectedConnectionType 60 * @return MockObject|LBFactory 61 */ 62 private function getMockLBFactory( 63 $mockDb, 64 $expectedConnectionType = null 65 ) { 66 $loadBalancer = $this->getMockLoadBalancer( $mockDb, $expectedConnectionType ); 67 $mock = $this->createMock( LBFactory::class ); 68 $mock->method( 'getMainLB' ) 69 ->willReturn( $loadBalancer ); 70 return $mock; 71 } 72 73 /** 74 * The job queue is used in three different places - two "push" calls, and a 75 * "lazyPush" call - we don't test any of the "push" calls, so the callback 76 * can just run the job, but we do test the "lazyPush" call, and so the test 77 * that is using this may want to do something other than just run the job, since 78 * for ActivityUpdateJob instances this results in using global functions, which we 79 * cannot do in this unit test 80 * 81 * @param bool $mockLazyPush whether to add mock behavior for "lazyPush" 82 * @return MockObject|JobQueueGroup 83 */ 84 private function getMockJobQueueGroup( $mockLazyPush = true ) { 85 $mock = $this->createMock( JobQueueGroup::class ); 86 $mock->method( 'push' ) 87 ->willReturnCallback( static function ( Job $job ) { 88 $job->run(); 89 } ); 90 if ( $mockLazyPush ) { 91 $mock->method( 'lazyPush' ) 92 ->willReturnCallback( static function ( Job $job ) { 93 $job->run(); 94 } ); 95 } 96 return $mock; 97 } 98 99 /** 100 * @return MockObject|HashBagOStuff 101 */ 102 private function getMockCache() { 103 $mock = $this->getMockBuilder( HashBagOStuff::class ) 104 ->disableOriginalConstructor() 105 ->onlyMethods( [ 'get', 'set', 'delete', 'makeKey' ] ) 106 ->getMock(); 107 $mock->method( 'makeKey' ) 108 ->willReturnCallback( static function ( ...$args ) { 109 return implode( ':', $args ); 110 } ); 111 return $mock; 112 } 113 114 /** 115 * No methods may be called except provided callbacks, if any. 116 * 117 * @param array $callbacks Keys are method names, values are callbacks 118 * @param array $counts Keys are method names, values are expected number of times to be called 119 * (default is any number is okay) 120 * @return MockObject|RevisionLookup 121 */ 122 private function getMockRevisionLookup( 123 array $callbacks = [], 124 array $counts = [] 125 ): RevisionLookup { 126 $mock = $this->createMock( RevisionLookup::class ); 127 foreach ( $callbacks as $method => $callback ) { 128 $count = isset( $counts[$method] ) ? $this->exactly( $counts[$method] ) : $this->any(); 129 $mock->expects( $count ) 130 ->method( $method ) 131 ->willReturnCallback( $callbacks[$method] ); 132 } 133 $mock->expects( $this->never() ) 134 ->method( $this->anythingBut( ...array_keys( $callbacks ) ) ); 135 return $mock; 136 } 137 138 /** 139 * @param IDatabase $mockDb 140 * @return LinkBatchFactory 141 */ 142 private function getMockLinkBatchFactory( $mockDb ) { 143 return new LinkBatchFactory( 144 $this->createMock( LinkCache::class ), 145 $this->createMock( TitleFormatter::class ), 146 $this->createMock( Language::class ), 147 $this->createMock( GenderCache::class ), 148 $this->getMockLoadBalancer( $mockDb ) 149 ); 150 } 151 152 /** 153 * @param User[] $users 154 * @return MockObject|UserFactory 155 */ 156 private function getUserFactory( array $users = [] ) { 157 // UserFactory is only needed for newFromUserIdentity. Create a mock User object 158 // based on the UserIdentity. Used for WatchedItemStore::resetNotificationTimestamp 159 // which needs full User objects for a hook. Mock users returned have the same 160 // name and id, and pass User::equals() comparison with the UserIdentity they were 161 // created from. 162 $userFactory = $this->createNoOpMock( UserFactory::class, [ 'newFromUserIdentity' ] ); 163 $userFactory->method( 'newFromUserIdentity' )->willReturnCallback( 164 function ( $userIdentity ) { 165 // Like real UserFactory, return $userIdentity if its a User 166 if ( $userIdentity instanceof User ) { 167 return $userIdentity; 168 } 169 170 $user = $this->createMock( User::class ); 171 $user->method( 'getId' )->willReturn( $userIdentity->getId() ); 172 $user->method( 'getName' )->willReturn( $userIdentity->getName() ); 173 $user->method( 'equals' )->willReturnCallback( 174 static function ( UserIdentity $otherUser ) use ( $userIdentity ) { 175 // $user's name is the same as $userIdentity's 176 return $otherUser->getName() === $userIdentity->getName(); 177 } 178 ); 179 return $user; 180 } 181 ); 182 return $userFactory; 183 } 184 185 /** 186 * @param LinkTarget|PageIdentity|null $target 187 * @param Title|null $title 188 * @return MockObject|TitleFactory 189 */ 190 private function getTitleFactory( $target = null, $title = null ) { 191 // TitleFactory only needed for castFromLinkTarget or castFromPageIdentity - if this is 192 // called with a link target or page identity and a title, the mock expects the function 193 // invocation and returns the title, otherwise the mock expects never to be called. 194 // If no title is provided here, we create a placeholder mock that passes the ->equals() 195 // check, and thats it 196 $titleFactory = $this->createNoOpMock( 197 TitleFactory::class, 198 [ 199 'castFromLinkTarget', 200 'castFromPageIdentity' 201 ] 202 ); 203 if ( $target !== null ) { 204 if ( $title === null ) { 205 $title = $this->makeMockTitle( 206 $target->getDBkey(), 207 [ 208 'namespace' => $target->getNamespace() 209 ] 210 ); 211 } 212 $title->method( 'equals' ) 213 ->with( $target ) 214 ->willReturn( true ); 215 if ( $target instanceof LinkTarget ) { 216 $titleFactory->method( 'castFromLinkTarget' ) 217 ->with( $target ) 218 ->willReturn( $title ); 219 $titleFactory->expects( $this->never() )->method( 'castFromPageIdentity' ); 220 } else { 221 $titleFactory->method( 'castFromPageIdentity' ) 222 ->with( $target ) 223 ->willReturn( $title ); 224 $titleFactory->expects( $this->never() )->method( 'castFromLinkTarget' ); 225 } 226 } else { 227 $titleFactory->expects( $this->never() )->method( 'castFromLinkTarget' ); 228 $titleFactory->expects( $this->never() )->method( 'castFromPageIdentity' ); 229 } 230 return $titleFactory; 231 } 232 233 /** 234 * @param array $mocks Associative array providing mocks to use when constructing the 235 * WatchedItemStore. Anything not provided will fall back to a default. Valid keys: 236 * * lbFactory 237 * * db 238 * * queueGroup 239 * * cache 240 * * readOnlyMode 241 * * nsInfo 242 * * revisionLookup 243 * * userFactory 244 * * titleFactory 245 * * expiryEnabled 246 * * maxExpiryDuration 247 * * watchlistPurgeRate 248 * @return WatchedItemStore 249 */ 250 private function newWatchedItemStore( array $mocks = [] ): WatchedItemStore { 251 $options = new ServiceOptions( WatchedItemStore::CONSTRUCTOR_OPTIONS, [ 252 'UpdateRowsPerQuery' => 1000, 253 'WatchlistExpiry' => $mocks['expiryEnabled'] ?? true, 254 'WatchlistExpiryMaxDuration' => $mocks['maxExpiryDuration'] ?? null, 255 'WatchlistPurgeRate' => $mocks['watchlistPurgeRate'] ?? 0.1, 256 ] ); 257 258 $db = $mocks['db'] ?? $this->getMockDb(); 259 260 // If we don't use a manual mock for something specific, get a full 261 // NamespaceInfo service from DummyServicesTrait::getDummyNamespaceInfo 262 $nsInfo = $mocks['nsInfo'] ?? $this->getDummyNamespaceInfo(); 263 264 return new WatchedItemStore( 265 $options, 266 $mocks['lbFactory'] ?? 267 $this->getMockLBFactory( $db ), 268 $mocks['queueGroup'] ?? $this->getMockJobQueueGroup(), 269 new HashBagOStuff(), 270 $mocks['cache'] ?? $this->getMockCache(), 271 $mocks['readOnlyMode'] ?? $this->getDummyReadOnlyMode( false ), 272 $nsInfo, 273 $mocks['revisionLookup'] ?? $this->getMockRevisionLookup(), 274 $this->createHookContainer(), 275 $this->getMockLinkBatchFactory( $db ), 276 $this->getUserFactory(), 277 $mocks['titleFactory'] ?? $this->getTitleFactory() 278 ); 279 } 280 281 public function testClearWatchedItems() { 282 $user = new UserIdentityValue( 7, 'MockUser' ); 283 284 $mockDb = $this->getMockDb(); 285 $mockDb->expects( $this->once() ) 286 ->method( 'selectField' ) 287 ->with( 288 [ 'watchlist' ], 289 'COUNT(*)', 290 [ 291 'wl_user' => $user->getId(), 292 ], 293 $this->isType( 'string' ) 294 ) 295 ->willReturn( 12 ); 296 $mockDb->expects( $this->once() ) 297 ->method( 'delete' ) 298 ->with( 299 'watchlist', 300 [ 'wl_user' => 7 ], 301 $this->isType( 'string' ) 302 ); 303 304 $mockCache = $this->getMockCache(); 305 $mockCache->expects( $this->never() )->method( 'get' ); 306 $mockCache->expects( $this->never() )->method( 'set' ); 307 $mockCache->expects( $this->once() ) 308 ->method( 'delete' ) 309 ->with( 'RM-KEY' ); 310 311 $store = $this->newWatchedItemStore( [ 312 'db' => $mockDb, 313 'cache' => $mockCache, 314 'expiryEnabled' => false, 315 ] ); 316 TestingAccessWrapper::newFromObject( $store ) 317 ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ]; 318 319 $this->assertTrue( $store->clearUserWatchedItems( $user ) ); 320 } 321 322 public function testClearWatchedItems_watchlistExpiry() { 323 $user = new UserIdentityValue( 7, 'MockUser' ); 324 325 $mockDb = $this->getMockDb(); 326 // Select watchlist IDs. 327 $mockDb->expects( $this->once() ) 328 ->method( 'selectFieldValues' ) 329 ->willReturn( [ 1, 2 ] ); 330 331 $mockDb->expects( $this->exactly( 2 ) ) 332 ->method( 'delete' ) 333 ->withConsecutive( 334 [ 335 'watchlist', 336 [ 'wl_id' => [ 1, 2 ] ] 337 ], 338 [ 339 'watchlist_expiry', 340 [ 'we_item' => [ 1, 2 ] ] 341 ] 342 ); 343 344 $mockCache = $this->getMockCache(); 345 $mockCache->expects( $this->never() )->method( 'get' ); 346 $mockCache->expects( $this->never() )->method( 'set' ); 347 $mockCache->expects( $this->once() ) 348 ->method( 'delete' ) 349 ->with( 'RM-KEY' ); 350 351 $store = $this->newWatchedItemStore( [ 352 'db' => $mockDb, 353 'cache' => $mockCache, 354 'expiryEnabled' => true, 355 ] ); 356 TestingAccessWrapper::newFromObject( $store ) 357 ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ]; 358 359 $this->assertTrue( $store->clearUserWatchedItems( $user ) ); 360 } 361 362 public function testClearWatchedItems_tooManyItemsWatched() { 363 $user = new UserIdentityValue( 7, 'MockUser' ); 364 365 $mockDb = $this->getMockDb(); 366 $mockDb->expects( $this->once() ) 367 ->method( 'selectField' ) 368 ->with( 369 [ 'watchlist' ], 370 'COUNT(*)', 371 [ 372 'wl_user' => $user->getId(), 373 ], 374 $this->isType( 'string' ) 375 ) 376 ->willReturn( 99999 ); 377 378 $mockCache = $this->getMockCache(); 379 $mockCache->expects( $this->never() )->method( 'get' ); 380 $mockCache->expects( $this->never() )->method( 'set' ); 381 $mockCache->expects( $this->never() )->method( 'delete' ); 382 383 $store = $this->newWatchedItemStore( [ 384 'db' => $mockDb, 385 'cache' => $mockCache, 386 'expiryEnabled' => false, 387 ] ); 388 389 $this->assertFalse( $store->clearUserWatchedItems( $user ) ); 390 } 391 392 public function testCountWatchedItems() { 393 $user = new UserIdentityValue( 1, 'MockUser' ); 394 395 $mockDb = $this->getMockDb(); 396 $mockDb->expects( $this->once() ) 397 ->method( 'addQuotes' ) 398 ->willReturn( '20200101000000' ); 399 $mockDb->expects( $this->once() ) 400 ->method( 'selectField' ) 401 ->with( 402 [ 'watchlist', 'watchlist_expiry' ], 403 'COUNT(*)', 404 [ 405 'wl_user' => $user->getId(), 406 'we_expiry IS NULL OR we_expiry > 20200101000000' 407 ], 408 $this->isType( 'string' ) 409 ) 410 ->willReturn( '12' ); 411 412 $mockCache = $this->getMockCache(); 413 $mockCache->expects( $this->never() )->method( 'get' ); 414 $mockCache->expects( $this->never() )->method( 'set' ); 415 $mockCache->expects( $this->never() )->method( 'delete' ); 416 417 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 418 419 $this->assertEquals( 12, $store->countWatchedItems( $user ) ); 420 } 421 422 public function provideTestPageFactory() { 423 yield [ static function ( $pageId, $namespace, $dbKey ) { 424 return new TitleValue( $namespace, $dbKey ); 425 } ]; 426 yield [ static function ( $pageId, $namespace, $dbKey ) { 427 return new PageIdentityValue( $pageId, $namespace, $dbKey, PageIdentityValue::LOCAL ); 428 } ]; 429 yield [ function ( $pageId, $namespace, $dbKey ) { 430 return $this->makeMockTitle( $dbKey, [ 431 'id' => $pageId, 432 'namespace' => $namespace 433 ] ); 434 } ]; 435 } 436 437 /** 438 * @dataProvider provideTestPageFactory 439 */ 440 public function testCountWatchers( $testPageFactory ) { 441 $titleValue = $testPageFactory( 100, 0, 'SomeDbKey' ); 442 443 $mockDb = $this->getMockDb(); 444 $mockDb->expects( $this->once() ) 445 ->method( 'addQuotes' ) 446 ->willReturn( '20200101000000' ); 447 $mockDb->expects( $this->once() ) 448 ->method( 'selectField' ) 449 ->with( 450 [ 'watchlist', 'watchlist_expiry' ], 451 'COUNT(*)', 452 [ 453 'wl_namespace' => $titleValue->getNamespace(), 454 'wl_title' => $titleValue->getDBkey(), 455 'we_expiry IS NULL OR we_expiry > 20200101000000' 456 ], 457 $this->isType( 'string' ) 458 ) 459 ->willReturn( '7' ); 460 461 $mockCache = $this->getMockCache(); 462 $mockCache->expects( $this->never() )->method( 'get' ); 463 $mockCache->expects( $this->never() )->method( 'set' ); 464 $mockCache->expects( $this->never() )->method( 'delete' ); 465 466 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 467 468 $this->assertEquals( 7, $store->countWatchers( $titleValue ) ); 469 } 470 471 /** 472 * @dataProvider provideTestPageFactory 473 */ 474 public function testCountWatchersMultiple( $testPageFactory ) { 475 $titleValues = [ 476 $testPageFactory( 100, 0, 'SomeDbKey' ), 477 $testPageFactory( 101, 0, 'OtherDbKey' ), 478 $testPageFactory( 102, 1, 'AnotherDbKey' ), 479 ]; 480 481 $mockDb = $this->getMockDb(); 482 483 $dbResult = [ 484 (object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ], 485 (object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ], 486 (object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ], 487 ]; 488 $mockDb->expects( $this->once() ) 489 ->method( 'makeWhereFrom2d' ) 490 ->with( 491 [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], 492 $this->isType( 'string' ), 493 $this->isType( 'string' ) 494 ) 495 ->willReturn( 'makeWhereFrom2d return value' ); 496 497 $mockDb->expects( $this->once() ) 498 ->method( 'addQuotes' ) 499 ->willReturn( '20200101000000' ); 500 501 $mockDb->expects( $this->once() ) 502 ->method( 'select' ) 503 ->with( 504 [ 'watchlist', 'watchlist_expiry' ], 505 [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ], 506 [ 507 'makeWhereFrom2d return value', 508 'we_expiry IS NULL OR we_expiry > 20200101000000' 509 ], 510 $this->isType( 'string' ), 511 [ 512 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], 513 ] 514 ) 515 ->willReturn( $dbResult ); 516 517 $mockCache = $this->getMockCache(); 518 $mockCache->expects( $this->never() )->method( 'get' ); 519 $mockCache->expects( $this->never() )->method( 'set' ); 520 $mockCache->expects( $this->never() )->method( 'delete' ); 521 522 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 523 524 $expected = [ 525 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], 526 1 => [ 'AnotherDbKey' => 500 ], 527 ]; 528 $this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) ); 529 } 530 531 public function provideIntWithDbUnsafeVersion() { 532 return [ 533 [ 50 ], 534 [ "50; DROP TABLE watchlist;\n--" ], 535 ]; 536 } 537 538 public function provideTestPageFactoryAndIntWithDbUnsafeVersion() { 539 foreach ( $this->provideIntWithDBUnsafeVersion() as $dbint ) { 540 foreach ( $this->provideTestPageFactory() as $testPageFactory ) { 541 yield [ $dbint[0], $testPageFactory[0] ]; 542 } 543 } 544 } 545 546 /** 547 * @dataProvider provideTestPageFactoryAndIntWithDbUnsafeVersion 548 */ 549 public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers, $testPageFactory ) { 550 $titleValues = [ 551 $testPageFactory( 100, 0, 'SomeDbKey' ), 552 $testPageFactory( 101, 0, 'OtherDbKey' ), 553 $testPageFactory( 102, 1, 'AnotherDbKey' ), 554 ]; 555 556 $mockDb = $this->getMockDb(); 557 558 $dbResult = [ 559 (object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ], 560 (object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ], 561 (object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ], 562 ]; 563 564 $mockDb->expects( $this->once() ) 565 ->method( 'makeWhereFrom2d' ) 566 ->with( 567 [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], 568 $this->isType( 'string' ), 569 $this->isType( 'string' ) 570 ) 571 ->willReturn( 'makeWhereFrom2d return value' ); 572 573 $mockDb->expects( $this->once() ) 574 ->method( 'addQuotes' ) 575 ->willReturn( '20200101000000' ); 576 577 $mockDb->expects( $this->once() ) 578 ->method( 'select' ) 579 ->with( 580 [ 'watchlist', 'watchlist_expiry' ], 581 [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ], 582 [ 583 'makeWhereFrom2d return value', 584 'we_expiry IS NULL OR we_expiry > 20200101000000' 585 ], 586 $this->isType( 'string' ), 587 [ 588 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], 589 'HAVING' => 'COUNT(*) >= 50', 590 ] 591 ) 592 ->willReturn( $dbResult ); 593 594 $mockCache = $this->getMockCache(); 595 $mockCache->expects( $this->never() )->method( 'get' ); 596 $mockCache->expects( $this->never() )->method( 'set' ); 597 $mockCache->expects( $this->never() )->method( 'delete' ); 598 599 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 600 601 $expected = [ 602 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], 603 1 => [ 'AnotherDbKey' => 500 ], 604 ]; 605 $this->assertEquals( 606 $expected, 607 $store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] ) 608 ); 609 } 610 611 /** 612 * @dataProvider provideTestPageFactory 613 */ 614 public function testCountVisitingWatchers( $testPageFactory ) { 615 $titleValue = $testPageFactory( 100, 0, 'SomeDbKey' ); 616 617 $mockDb = $this->getMockDb(); 618 619 $mockDb->expects( $this->once() ) 620 ->method( 'selectField' ) 621 ->with( 622 [ 'watchlist', 'watchlist_expiry' ], 623 'COUNT(*)', 624 [ 625 'wl_namespace' => $titleValue->getNamespace(), 626 'wl_title' => $titleValue->getDBkey(), 627 'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL', 628 'we_expiry IS NULL OR we_expiry > \'20200101000000\'' 629 ], 630 $this->isType( 'string' ) 631 ) 632 ->willReturn( '7' ); 633 634 $mockDb->expects( $this->exactly( 2 ) ) 635 ->method( 'addQuotes' ) 636 ->willReturnCallback( static function ( $value ) { 637 return "'$value'"; 638 } ); 639 640 $mockDb->expects( $this->exactly( 2 ) ) 641 ->method( 'timestamp' ) 642 ->willReturnCallback( static function ( $value ) { 643 if ( $value === 0 ) { 644 return '20200101000000'; 645 } 646 return 'TS' . $value . 'TS'; 647 } ); 648 649 $mockCache = $this->getMockCache(); 650 $mockCache->expects( $this->never() )->method( 'set' ); 651 $mockCache->expects( $this->never() )->method( 'get' ); 652 $mockCache->expects( $this->never() )->method( 'delete' ); 653 654 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 655 656 $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) ); 657 } 658 659 /** 660 * @dataProvider provideTestPageFactory 661 */ 662 public function testCountVisitingWatchersMultiple( $testPageFactory ) { 663 $titleValuesWithThresholds = [ 664 [ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ], 665 [ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ], 666 [ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ], 667 ]; 668 669 $dbResult = [ 670 (object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ], 671 (object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ], 672 (object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ], 673 ]; 674 $mockDb = $this->getMockDb(); 675 $mockDb->expects( $this->exactly( 2 * 3 + 1 ) ) 676 ->method( 'addQuotes' ) 677 ->willReturnCallback( static function ( $value ) { 678 return "'$value'"; 679 } ); 680 681 $mockDb->expects( $this->exactly( 4 ) ) 682 ->method( 'timestamp' ) 683 ->willReturnCallback( static function ( $value ) { 684 if ( $value === 0 ) { 685 return '20200101000000'; 686 } 687 return 'TS' . $value . 'TS'; 688 } ); 689 690 $mockDb->method( 'makeList' ) 691 ->with( 692 $this->isType( 'array' ), 693 $this->isType( 'int' ) 694 ) 695 ->willReturnCallback( static function ( $a, $conj ) { 696 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; 697 return implode( $sqlConj, array_map( static function ( $s ) { 698 return '(' . $s . ')'; 699 }, $a 700 ) ); 701 } ); 702 $mockDb->expects( $this->never() ) 703 ->method( 'makeWhereFrom2d' ); 704 705 $expectedCond = 706 '((wl_namespace = 0) AND (' . 707 "(((wl_title = 'SomeDbKey') AND (" . 708 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . 709 ')) OR (' . 710 "(wl_title = 'OtherDbKey') AND (" . 711 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . 712 '))))' . 713 ') OR ((wl_namespace = 1) AND (' . 714 "(((wl_title = 'AnotherDbKey') AND (" . 715 "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" . 716 ')))))'; 717 $mockDb->expects( $this->once() ) 718 ->method( 'select' ) 719 ->with( 720 [ 'watchlist', 'watchlist_expiry' ], 721 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], 722 [ 723 $expectedCond, 724 'we_expiry IS NULL OR we_expiry > \'20200101000000\'' 725 ], 726 $this->isType( 'string' ), 727 [ 728 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], 729 ] 730 ) 731 ->willReturn( $dbResult ); 732 733 $mockCache = $this->getMockCache(); 734 $mockCache->expects( $this->never() )->method( 'get' ); 735 $mockCache->expects( $this->never() )->method( 'set' ); 736 $mockCache->expects( $this->never() )->method( 'delete' ); 737 738 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 739 740 $expected = [ 741 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], 742 1 => [ 'AnotherDbKey' => 500 ], 743 ]; 744 $this->assertEquals( 745 $expected, 746 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds ) 747 ); 748 } 749 750 /** 751 * @dataProvider provideTestPageFactory 752 */ 753 public function testCountVisitingWatchersMultiple_withMissingTargets( $testPageFactory ) { 754 $titleValuesWithThresholds = [ 755 [ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ], 756 [ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ], 757 [ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ], 758 [ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ], 759 [ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ], 760 ]; 761 762 $dbResult = [ 763 (object)[ 'wl_title' => 'SomeDbKey', 'wl_namespace' => '0', 'watchers' => '100' ], 764 (object)[ 'wl_title' => 'OtherDbKey', 'wl_namespace' => '0', 'watchers' => '300' ], 765 (object)[ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => '1', 'watchers' => '500' ], 766 (object)[ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '100' ], 767 (object)[ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => '0', 'watchers' => '200' ], 768 ]; 769 $mockDb = $this->getMockDb(); 770 $mockDb->expects( $this->exactly( 2 * 3 ) ) 771 ->method( 'addQuotes' ) 772 ->willReturnCallback( static function ( $value ) { 773 return "'$value'"; 774 } ); 775 $mockDb->expects( $this->exactly( 3 ) ) 776 ->method( 'timestamp' ) 777 ->willReturnCallback( static function ( $value ) { 778 return 'TS' . $value . 'TS'; 779 } ); 780 $mockDb->method( 'makeList' ) 781 ->with( 782 $this->isType( 'array' ), 783 $this->isType( 'int' ) 784 ) 785 ->willReturnCallback( static function ( $a, $conj ) { 786 $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; 787 return implode( $sqlConj, array_map( static function ( $s ) { 788 return '(' . $s . ')'; 789 }, $a 790 ) ); 791 } ); 792 $mockDb->expects( $this->once() ) 793 ->method( 'makeWhereFrom2d' ) 794 ->with( 795 [ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ], 796 $this->isType( 'string' ), 797 $this->isType( 'string' ) 798 ) 799 ->willReturn( 'makeWhereFrom2d return value' ); 800 801 $expectedCond = 802 '((wl_namespace = 0) AND (' . 803 "(((wl_title = 'SomeDbKey') AND (" . 804 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . 805 ')) OR (' . 806 "(wl_title = 'OtherDbKey') AND (" . 807 "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . 808 '))))' . 809 ') OR ((wl_namespace = 1) AND (' . 810 "(((wl_title = 'AnotherDbKey') AND (" . 811 "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" . 812 '))))' . 813 ') OR ' . 814 '(makeWhereFrom2d return value)'; 815 $mockDb->expects( $this->once() ) 816 ->method( 'select' ) 817 ->with( 818 [ 'watchlist' ], 819 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], 820 [ $expectedCond ], 821 $this->isType( 'string' ), 822 [ 823 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], 824 ] 825 ) 826 ->willReturn( $dbResult ); 827 828 $mockCache = $this->getMockCache(); 829 $mockCache->expects( $this->never() )->method( 'get' ); 830 $mockCache->expects( $this->never() )->method( 'set' ); 831 $mockCache->expects( $this->never() )->method( 'delete' ); 832 833 $store = $this->newWatchedItemStore( [ 834 'db' => $mockDb, 835 'cache' => $mockCache, 836 'expiryEnabled' => false 837 ] ); 838 839 $expected = [ 840 0 => [ 841 'SomeDbKey' => 100, 'OtherDbKey' => 300, 842 'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200 843 ], 844 1 => [ 'AnotherDbKey' => 500 ], 845 ]; 846 $this->assertEquals( 847 $expected, 848 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds ) 849 ); 850 } 851 852 /** 853 * @dataProvider provideTestPageFactoryAndIntWithDbUnsafeVersion 854 */ 855 public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers, $testPageFactory ) { 856 $titleValuesWithThresholds = [ 857 [ $testPageFactory( 100, 0, 'SomeDbKey' ), '111' ], 858 [ $testPageFactory( 101, 0, 'OtherDbKey' ), '111' ], 859 [ $testPageFactory( 102, 1, 'AnotherDbKey' ), '123' ], 860 ]; 861 862 $mockDb = $this->getMockDb(); 863 $mockDb->method( 'makeList' ) 864 ->willReturn( 'makeList return value' ); 865 $mockDb->expects( $this->once() ) 866 ->method( 'select' ) 867 ->with( 868 [ 'watchlist' ], 869 [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], 870 [ 'makeList return value' ], 871 $this->isType( 'string' ), 872 [ 873 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], 874 'HAVING' => 'COUNT(*) >= 50', 875 ] 876 ) 877 ->willReturn( [] ); 878 879 $mockCache = $this->getMockCache(); 880 $mockCache->expects( $this->never() )->method( 'get' ); 881 $mockCache->expects( $this->never() )->method( 'set' ); 882 $mockCache->expects( $this->never() )->method( 'delete' ); 883 884 $store = $this->newWatchedItemStore( [ 885 'db' => $mockDb, 886 'cache' => $mockCache, 887 'expiryEnabled' => false, 888 ] ); 889 890 $expected = [ 891 0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ], 892 1 => [ 'AnotherDbKey' => 0 ], 893 ]; 894 $this->assertEquals( 895 $expected, 896 $store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers ) 897 ); 898 } 899 900 public function testCountUnreadNotifications() { 901 $user = new UserIdentityValue( 1, 'MockUser' ); 902 903 $mockDb = $this->getMockDb(); 904 $mockDb->expects( $this->once() ) 905 ->method( 'selectRowCount' ) 906 ->with( 907 'watchlist', 908 '1', 909 [ 910 "wl_notificationtimestamp IS NOT NULL", 911 'wl_user' => 1, 912 ], 913 $this->isType( 'string' ) 914 ) 915 ->willReturn( '9' ); 916 917 $mockCache = $this->getMockCache(); 918 $mockCache->expects( $this->never() )->method( 'set' ); 919 $mockCache->expects( $this->never() )->method( 'get' ); 920 $mockCache->expects( $this->never() )->method( 'delete' ); 921 922 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 923 924 $this->assertEquals( 9, $store->countUnreadNotifications( $user ) ); 925 } 926 927 /** 928 * @dataProvider provideIntWithDbUnsafeVersion 929 */ 930 public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) { 931 $user = new UserIdentityValue( 1, 'MockUser' ); 932 933 $mockDb = $this->getMockDb(); 934 $mockDb->expects( $this->once() ) 935 ->method( 'selectRowCount' ) 936 ->with( 937 'watchlist', 938 '1', 939 [ 940 "wl_notificationtimestamp IS NOT NULL", 941 'wl_user' => 1, 942 ], 943 $this->isType( 'string' ), 944 [ 'LIMIT' => 50 ] 945 ) 946 ->willReturn( '50' ); 947 948 $mockCache = $this->getMockCache(); 949 $mockCache->expects( $this->never() )->method( 'set' ); 950 $mockCache->expects( $this->never() )->method( 'get' ); 951 $mockCache->expects( $this->never() )->method( 'delete' ); 952 953 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 954 955 $this->assertSame( 956 true, 957 $store->countUnreadNotifications( $user, $limit ) 958 ); 959 } 960 961 /** 962 * @dataProvider provideIntWithDbUnsafeVersion 963 */ 964 public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) { 965 $user = new UserIdentityValue( 1, 'MockUser' ); 966 967 $mockDb = $this->getMockDb(); 968 $mockDb->expects( $this->once() ) 969 ->method( 'selectRowCount' ) 970 ->with( 971 'watchlist', 972 '1', 973 [ 974 "wl_notificationtimestamp IS NOT NULL", 975 'wl_user' => 1, 976 ], 977 $this->isType( 'string' ), 978 [ 'LIMIT' => 50 ] 979 ) 980 ->willReturn( '9' ); 981 982 $mockCache = $this->getMockCache(); 983 $mockCache->expects( $this->never() )->method( 'set' ); 984 $mockCache->expects( $this->never() )->method( 'get' ); 985 $mockCache->expects( $this->never() )->method( 'delete' ); 986 987 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 988 989 $this->assertEquals( 990 9, 991 $store->countUnreadNotifications( $user, $limit ) 992 ); 993 } 994 995 /** 996 * @dataProvider provideTestPageFactory 997 */ 998 public function testDuplicateEntry_nothingToDuplicate( $testPageFactory ) { 999 $mockDb = $this->getMockDb(); 1000 $mockDb->expects( $this->once() ) 1001 ->method( 'select' ) 1002 ->with( 1003 [ 'watchlist', 'watchlist_expiry' ], 1004 [ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ], 1005 [ 1006 'wl_namespace' => 0, 1007 'wl_title' => 'Old_Title', 1008 ], 1009 'WatchedItemStore::fetchWatchedItemsForPage', 1010 [ 'FOR UPDATE' ], 1011 [ 'watchlist_expiry' => [ 'LEFT JOIN', [ 'wl_id = we_item' ] ] ] 1012 ) 1013 ->willReturn( new FakeResultWrapper( [] ) ); 1014 1015 $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] ); 1016 1017 $store->duplicateEntry( 1018 $testPageFactory( 100, 0, 'Old_Title' ), 1019 $testPageFactory( 101, 0, 'New_Title' ) 1020 ); 1021 } 1022 1023 /** 1024 * @dataProvider provideTestPageFactory 1025 */ 1026 public function testDuplicateEntry_somethingToDuplicate( $testPageFactory ) { 1027 $fakeRows = [ 1028 (object)[ 1029 'wl_user' => '1', 1030 'wl_notificationtimestamp' => '20151212010101', 1031 ], 1032 (object)[ 1033 'wl_user' => '2', 1034 'wl_notificationtimestamp' => null, 1035 ], 1036 ]; 1037 1038 $mockDb = $this->getMockDb(); 1039 $mockDb->expects( $this->at( 0 ) ) 1040 ->method( 'select' ) 1041 ->with( 1042 [ 'watchlist' ], 1043 [ 'wl_user', 'wl_notificationtimestamp' ], 1044 [ 1045 'wl_namespace' => 0, 1046 'wl_title' => 'Old_Title', 1047 ] 1048 ) 1049 ->willReturn( new FakeResultWrapper( $fakeRows ) ); 1050 $mockDb->expects( $this->at( 1 ) ) 1051 ->method( 'replace' ) 1052 ->with( 1053 'watchlist', 1054 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], 1055 [ 1056 [ 1057 'wl_user' => 1, 1058 'wl_namespace' => 0, 1059 'wl_title' => 'New_Title', 1060 'wl_notificationtimestamp' => '20151212010101', 1061 ], 1062 [ 1063 'wl_user' => 2, 1064 'wl_namespace' => 0, 1065 'wl_title' => 'New_Title', 1066 'wl_notificationtimestamp' => null, 1067 ], 1068 ], 1069 $this->isType( 'string' ) 1070 ); 1071 1072 $mockCache = $this->getMockCache(); 1073 $mockCache->expects( $this->never() )->method( 'get' ); 1074 $mockCache->expects( $this->never() )->method( 'delete' ); 1075 1076 $store = $this->newWatchedItemStore( [ 1077 'db' => $mockDb, 1078 'cache' => $mockCache, 1079 'expiryEnabled' => false, 1080 ] ); 1081 1082 $store->duplicateEntry( 1083 $testPageFactory( 100, 0, 'Old_Title' ), 1084 $testPageFactory( 101, 0, 'New_Title' ) 1085 ); 1086 } 1087 1088 /** 1089 * @dataProvider provideTestPageFactory 1090 */ 1091 public function testDuplicateAllAssociatedEntries_nothingToDuplicate( $testPageFactory ) { 1092 $mockDb = $this->getMockDb(); 1093 $mockDb->expects( $this->at( 0 ) ) 1094 ->method( 'select' ) 1095 ->with( 1096 [ 'watchlist', 'watchlist_expiry' ], 1097 [ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ], 1098 [ 1099 'wl_namespace' => 0, 1100 'wl_title' => 'Old_Title', 1101 ] 1102 ) 1103 ->willReturn( new FakeResultWrapper( [] ) ); 1104 $mockDb->expects( $this->at( 1 ) ) 1105 ->method( 'select' ) 1106 ->with( 1107 [ 'watchlist', 'watchlist_expiry' ], 1108 [ 'wl_user', 'wl_notificationtimestamp', 'we_expiry' ], 1109 [ 1110 'wl_namespace' => 1, 1111 'wl_title' => 'Old_Title', 1112 ] 1113 ) 1114 ->willReturn( new FakeResultWrapper( [] ) ); 1115 1116 $mockCache = $this->getMockCache(); 1117 $mockCache->expects( $this->never() )->method( 'get' ); 1118 $mockCache->expects( $this->never() )->method( 'delete' ); 1119 1120 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1121 1122 $store->duplicateAllAssociatedEntries( 1123 $testPageFactory( 100, 0, 'Old_Title' ), 1124 $testPageFactory( 101, 0, 'New_Title' ) 1125 ); 1126 } 1127 1128 public function provideLinkTargetPairs() { 1129 foreach ( $this->provideTestPageFactory() as $testPageFactoryArray ) { 1130 $testPageFactory = $testPageFactoryArray[0]; 1131 yield [ $testPageFactory( 100, 0, 'Old_Title' ), $testPageFactory( 101, 0, 'New_Title' ) ]; 1132 } 1133 } 1134 1135 /** 1136 * @param LinkTarget|PageIdentity $oldTarget 1137 * @param LinkTarget|PageIdentity $newTarget 1138 * @dataProvider provideLinkTargetPairs 1139 */ 1140 public function testDuplicateAllAssociatedEntries_somethingToDuplicate( 1141 $oldTarget, 1142 $newTarget 1143 ) { 1144 $fakeRows = [ 1145 (object)[ 1146 'wl_user' => '1', 1147 'wl_notificationtimestamp' => '20151212010101', 1148 'we_expiry' => null, 1149 ], 1150 ]; 1151 1152 $mockDb = $this->getMockDb(); 1153 $mockDb->expects( $this->at( 0 ) ) 1154 ->method( 'select' ) 1155 ->with( 1156 [ 'watchlist' ], 1157 [ 'wl_user', 'wl_notificationtimestamp' ], 1158 [ 1159 'wl_namespace' => $oldTarget->getNamespace(), 1160 'wl_title' => $oldTarget->getDBkey(), 1161 ] 1162 ) 1163 ->willReturn( new FakeResultWrapper( $fakeRows ) ); 1164 $mockDb->expects( $this->at( 1 ) ) 1165 ->method( 'replace' ) 1166 ->with( 1167 'watchlist', 1168 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], 1169 [ 1170 [ 1171 'wl_user' => 1, 1172 'wl_namespace' => $newTarget->getNamespace(), 1173 'wl_title' => $newTarget->getDBkey(), 1174 'wl_notificationtimestamp' => '20151212010101', 1175 ], 1176 ], 1177 $this->isType( 'string' ) 1178 ); 1179 $mockDb->expects( $this->at( 2 ) ) 1180 ->method( 'select' ) 1181 ->with( 1182 [ 'watchlist' ], 1183 [ 'wl_user', 'wl_notificationtimestamp' ], 1184 [ 1185 'wl_namespace' => $oldTarget->getNamespace() + 1, 1186 'wl_title' => $oldTarget->getDBkey(), 1187 ] 1188 ) 1189 ->willReturn( new FakeResultWrapper( $fakeRows ) ); 1190 $mockDb->expects( $this->at( 3 ) ) 1191 ->method( 'replace' ) 1192 ->with( 1193 'watchlist', 1194 [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], 1195 [ 1196 [ 1197 'wl_user' => 1, 1198 'wl_namespace' => $newTarget->getNamespace() + 1, 1199 'wl_title' => $newTarget->getDBkey(), 1200 'wl_notificationtimestamp' => '20151212010101', 1201 ], 1202 ], 1203 $this->isType( 'string' ) 1204 ); 1205 1206 $mockCache = $this->getMockCache(); 1207 $mockCache->expects( $this->never() )->method( 'get' ); 1208 $mockCache->expects( $this->never() )->method( 'delete' ); 1209 1210 $store = $this->newWatchedItemStore( [ 1211 'db' => $mockDb, 1212 'cache' => $mockCache, 1213 'expiryEnabled' => false, 1214 ] ); 1215 1216 $store->duplicateAllAssociatedEntries( 1217 $oldTarget, 1218 $newTarget 1219 ); 1220 } 1221 1222 /** 1223 * @dataProvider provideTestPageFactory 1224 */ 1225 public function testAddWatch_nonAnonymousUser( $testPageFactory ) { 1226 $mockDb = $this->getMockDb(); 1227 $mockDb->expects( $this->once() ) 1228 ->method( 'insert' ) 1229 ->with( 1230 'watchlist', 1231 [ 1232 [ 1233 'wl_user' => 1, 1234 'wl_namespace' => 0, 1235 'wl_title' => 'Some_Page', 1236 'wl_notificationtimestamp' => null, 1237 ] 1238 ] 1239 ); 1240 1241 $mockCache = $this->getMockCache(); 1242 $mockCache->expects( $this->once() ) 1243 ->method( 'delete' ) 1244 ->with( '0:Some_Page:1' ); 1245 1246 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1247 1248 $store->addWatch( 1249 new UserIdentityValue( 1, 'MockUser' ), 1250 $testPageFactory( 100, 0, 'Some_Page' ) 1251 ); 1252 } 1253 1254 /** 1255 * @dataProvider provideTestPageFactory 1256 */ 1257 public function testAddWatch_anonymousUser( $testPageFactory ) { 1258 $mockDb = $this->getMockDb(); 1259 $mockDb->expects( $this->never() ) 1260 ->method( 'insert' ); 1261 1262 $mockCache = $this->getMockCache(); 1263 $mockCache->expects( $this->never() ) 1264 ->method( 'delete' ); 1265 1266 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1267 1268 $store->addWatch( 1269 new UserIdentityValue( 0, 'AnonUser' ), 1270 $testPageFactory( 100, 0, 'Some_Page' ) 1271 ); 1272 } 1273 1274 /** 1275 * @dataProvider provideTestPageFactory 1276 */ 1277 public function testAddWatchBatchForUser_readOnlyDBReturnsFalse( $testPageFactory ) { 1278 $store = $this->newWatchedItemStore( 1279 [ 'readOnlyMode' => $this->getDummyReadOnlyMode( true ) ] 1280 ); 1281 1282 $this->assertFalse( 1283 $store->addWatchBatchForUser( 1284 new UserIdentityValue( 1, 'MockUser' ), 1285 [ $testPageFactory( 100, 0, 'Some_Page' ), $testPageFactory( 101, 1, 'Some_Page' ) ] 1286 ) 1287 ); 1288 } 1289 1290 /** 1291 * @dataProvider provideTestPageFactory 1292 */ 1293 public function testAddWatchBatchForUser_nonAnonymousUser( $testPageFactory ) { 1294 $mockDb = $this->getMockDb(); 1295 $mockDb->expects( $this->once() ) 1296 ->method( 'insert' ) 1297 ->with( 1298 'watchlist', 1299 [ 1300 [ 1301 'wl_user' => 1, 1302 'wl_namespace' => 0, 1303 'wl_title' => 'Some_Page', 1304 'wl_notificationtimestamp' => null, 1305 ], 1306 [ 1307 'wl_user' => 1, 1308 'wl_namespace' => 1, 1309 'wl_title' => 'Some_Page', 1310 'wl_notificationtimestamp' => null, 1311 ] 1312 ] 1313 ); 1314 1315 $mockDb->expects( $this->once() ) 1316 ->method( 'affectedRows' ) 1317 ->willReturn( 2 ); 1318 1319 $mockCache = $this->getMockCache(); 1320 $mockCache->expects( $this->exactly( 2 ) ) 1321 ->method( 'delete' ); 1322 $mockCache->expects( $this->at( 1 ) ) 1323 ->method( 'delete' ) 1324 ->with( '0:Some_Page:1' ); 1325 $mockCache->expects( $this->at( 3 ) ) 1326 ->method( 'delete' ) 1327 ->with( '1:Some_Page:1' ); 1328 1329 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1330 1331 $mockUser = new UserIdentityValue( 1, 'MockUser' ); 1332 1333 $this->assertTrue( 1334 $store->addWatchBatchForUser( 1335 $mockUser, 1336 [ $testPageFactory( 100, 0, 'Some_Page' ), $testPageFactory( 101, 1, 'Some_Page' ) ] 1337 ) 1338 ); 1339 } 1340 1341 /** 1342 * @dataProvider provideTestPageFactory 1343 */ 1344 public function testAddWatchBatchForUser_anonymousUsersAreSkipped( $testPageFactory ) { 1345 $mockDb = $this->getMockDb(); 1346 $mockDb->expects( $this->never() ) 1347 ->method( 'insert' ); 1348 1349 $mockCache = $this->getMockCache(); 1350 $mockCache->expects( $this->never() ) 1351 ->method( 'delete' ); 1352 1353 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1354 1355 $this->assertFalse( 1356 $store->addWatchBatchForUser( 1357 new UserIdentityValue( 0, 'AnonUser' ), 1358 [ $testPageFactory( 100, 0, 'Other_Page' ) ] 1359 ) 1360 ); 1361 } 1362 1363 public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() { 1364 $user = new UserIdentityValue( 1, 'MockUser' ); 1365 $mockDb = $this->getMockDb(); 1366 $mockDb->expects( $this->never() ) 1367 ->method( 'insert' ); 1368 1369 $mockCache = $this->getMockCache(); 1370 $mockCache->expects( $this->never() ) 1371 ->method( 'delete' ); 1372 1373 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1374 1375 $this->assertTrue( 1376 $store->addWatchBatchForUser( $user, [] ) 1377 ); 1378 } 1379 1380 /** 1381 * @dataProvider provideTestPageFactory 1382 */ 1383 public function testLoadWatchedItem_existingItem( $testPageFactory ) { 1384 $mockDb = $this->getMockDb(); 1385 $mockDb->expects( $this->once() ) 1386 ->method( 'addQuotes' ) 1387 ->willReturn( '20200101000000' ); 1388 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 1389 $mockDb->expects( $this->exactly( 2 ) ) 1390 ->method( 'makeList' ) 1391 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 1392 $mockDb->expects( $this->once() ) 1393 ->method( 'select' ) 1394 ->with( 1395 [ 'watchlist', 'watchlist_expiry' ], 1396 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1397 [ 1398 'wl_user' => 1, 1399 $makeListSql, 1400 'we_expiry IS NULL OR we_expiry > 20200101000000' 1401 ] 1402 ) 1403 ->willReturn( [ 1404 (object)[ 1405 'wl_namespace' => 0, 1406 'wl_title' => 'SomeDbKey', 1407 'wl_notificationtimestamp' => '20151212010101', 1408 'we_expiry' => '20300101000000' 1409 ] 1410 ] ); 1411 1412 $mockCache = $this->getMockCache(); 1413 $mockCache->expects( $this->once() ) 1414 ->method( 'set' ) 1415 ->with( 1416 '0:SomeDbKey:1' 1417 ); 1418 1419 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1420 1421 $watchedItem = $store->loadWatchedItem( 1422 new UserIdentityValue( 1, 'MockUser' ), 1423 $testPageFactory( 100, 0, 'SomeDbKey' ) 1424 ); 1425 $this->assertInstanceOf( WatchedItem::class, $watchedItem ); 1426 $this->assertSame( 1, $watchedItem->getUserIdentity()->getId() ); 1427 $this->assertEquals( 'SomeDbKey', $watchedItem->getTarget()->getDBkey() ); 1428 $this->assertSame( '20300101000000', $watchedItem->getExpiry() ); 1429 $this->assertSame( 0, $watchedItem->getTarget()->getNamespace() ); 1430 } 1431 1432 /** 1433 * @dataProvider provideTestPageFactory 1434 */ 1435 public function testLoadWatchedItem_noItem( $testPageFactory ) { 1436 $mockDb = $this->getMockDb(); 1437 $mockDb->expects( $this->once() ) 1438 ->method( 'addQuotes' ) 1439 ->willReturn( '20200101000000' ); 1440 $mockDb->expects( $this->once() ) 1441 ->method( 'select' ) 1442 ->willReturn( [] ); 1443 1444 $mockCache = $this->getMockCache(); 1445 $mockCache->expects( $this->never() )->method( 'get' ); 1446 $mockCache->expects( $this->never() )->method( 'delete' ); 1447 1448 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1449 1450 $this->assertFalse( 1451 $store->loadWatchedItem( 1452 new UserIdentityValue( 1, 'MockUser' ), 1453 $testPageFactory( 100, 0, 'SomeDbKey' ) 1454 ) 1455 ); 1456 } 1457 1458 /** 1459 * @dataProvider provideTestPageFactory 1460 */ 1461 public function testLoadWatchedItem_anonymousUser( $testPageFactory ) { 1462 $mockDb = $this->getMockDb(); 1463 $mockDb->expects( $this->never() ) 1464 ->method( 'select' ); 1465 1466 $mockCache = $this->getMockCache(); 1467 $mockCache->expects( $this->never() )->method( 'get' ); 1468 $mockCache->expects( $this->never() )->method( 'delete' ); 1469 1470 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1471 1472 $this->assertFalse( 1473 $store->loadWatchedItem( 1474 new UserIdentityValue( 0, 'AnonUser' ), 1475 $testPageFactory( 100, 0, 'SomeDbKey' ) 1476 ) 1477 ); 1478 } 1479 1480 /** 1481 * @dataProvider provideTestPageFactory 1482 */ 1483 public function testRemoveWatch_existingItem( $testPageFactory ) { 1484 $mockDb = $this->getMockDb(); 1485 $mockDb->expects( $this->once() ) 1486 ->method( 'selectFieldValues' ) 1487 ->willReturn( [ 1, 2 ] ); 1488 $mockDb->expects( $this->exactly( 2 ) ) 1489 ->method( 'delete' ) 1490 ->withConsecutive( 1491 [ 1492 'watchlist', 1493 [ 'wl_id' => [ 1, 2 ] ] 1494 ], 1495 [ 1496 'watchlist_expiry', 1497 [ 'we_item' => [ 1, 2 ] ] 1498 ] 1499 ); 1500 $mockDb->expects( $this->exactly( 2 ) ) 1501 ->method( 'affectedRows' ) 1502 ->willReturn( 2 ); 1503 1504 $mockCache = $this->getMockCache(); 1505 $mockCache->expects( $this->never() )->method( 'get' ); 1506 $mockCache->expects( $this->once() ) 1507 ->method( 'delete' ) 1508 ->withConsecutive( 1509 [ '0:SomeDbKey:1' ], 1510 [ '1:SomeDbKey:1' ] 1511 ); 1512 1513 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1514 1515 $this->assertTrue( 1516 $store->removeWatch( 1517 new UserIdentityValue( 1, 'MockUser' ), 1518 $testPageFactory( 100, 0, 'SomeDbKey' ) 1519 ) 1520 ); 1521 } 1522 1523 /** 1524 * @dataProvider provideTestPageFactory 1525 */ 1526 public function testRemoveWatch_noItem( $testPageFactory ) { 1527 $mockDb = $this->getMockDb(); 1528 $mockDb->expects( $this->once() ) 1529 ->method( 'selectFieldValues' ) 1530 ->willReturn( [] ); 1531 $mockDb->expects( $this->never() ) 1532 ->method( 'delete' ); 1533 $mockDb->expects( $this->never() ) 1534 ->method( 'affectedRows' ); 1535 1536 $mockCache = $this->getMockCache(); 1537 $mockCache->expects( $this->never() )->method( 'get' ); 1538 $mockCache->expects( $this->once() ) 1539 ->method( 'delete' ) 1540 ->withConsecutive( 1541 [ '0:SomeDbKey:1' ], 1542 [ '1:SomeDbKey:1' ] 1543 ); 1544 1545 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1546 1547 $this->assertFalse( 1548 $store->removeWatch( 1549 new UserIdentityValue( 1, 'MockUser' ), 1550 $testPageFactory( 100, 0, 'SomeDbKey' ) 1551 ) 1552 ); 1553 } 1554 1555 /** 1556 * @dataProvider provideTestPageFactory 1557 */ 1558 public function testRemoveWatch_anonymousUser( $testPageFactory ) { 1559 $mockDb = $this->getMockDb(); 1560 $mockDb->expects( $this->never() ) 1561 ->method( 'delete' ); 1562 1563 $mockCache = $this->getMockCache(); 1564 $mockCache->expects( $this->never() )->method( 'get' ); 1565 $mockCache->expects( $this->never() ) 1566 ->method( 'delete' ); 1567 1568 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1569 1570 $this->assertFalse( 1571 $store->removeWatch( 1572 new UserIdentityValue( 0, 'AnonUser' ), 1573 $testPageFactory( 100, 0, 'SomeDbKey' ) 1574 ) 1575 ); 1576 } 1577 1578 /** 1579 * @dataProvider provideTestPageFactory 1580 */ 1581 public function testGetWatchedItem_existingItem( $testPageFactory ) { 1582 $mockDb = $this->getMockDb(); 1583 $mockDb->expects( $this->once() ) 1584 ->method( 'addQuotes' ) 1585 ->willReturn( '20200101000000' ); 1586 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 1587 $mockDb->expects( $this->exactly( 2 ) ) 1588 ->method( 'makeList' ) 1589 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 1590 $mockDb->expects( $this->once() ) 1591 ->method( 'select' ) 1592 ->with( 1593 [ 'watchlist', 'watchlist_expiry' ], 1594 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1595 [ 1596 'wl_user' => 1, 1597 $makeListSql, 1598 'we_expiry IS NULL OR we_expiry > 20200101000000' 1599 ] 1600 ) 1601 ->willReturn( [ 1602 (object)[ 1603 'wl_namespace' => 0, 1604 'wl_title' => 'SomeDbKey', 1605 'wl_notificationtimestamp' => '20151212010101', 1606 'we_expiry' => '20300101000000' 1607 ] 1608 ] ); 1609 1610 $mockCache = $this->getMockCache(); 1611 $mockCache->expects( $this->never() )->method( 'delete' ); 1612 $mockCache->expects( $this->once() ) 1613 ->method( 'get' ) 1614 ->with( 1615 '0:SomeDbKey:1' 1616 ) 1617 ->willReturn( null ); 1618 $mockCache->expects( $this->once() ) 1619 ->method( 'set' ) 1620 ->with( 1621 '0:SomeDbKey:1' 1622 ); 1623 1624 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1625 1626 $watchedItem = $store->getWatchedItem( 1627 new UserIdentityValue( 1, 'MockUser' ), 1628 $testPageFactory( 100, 0, 'SomeDbKey' ) 1629 ); 1630 $this->assertInstanceOf( WatchedItem::class, $watchedItem ); 1631 $this->assertSame( 1, $watchedItem->getUserIdentity()->getId() ); 1632 $this->assertEquals( 'SomeDbKey', $watchedItem->getTarget()->getDBkey() ); 1633 $this->assertSame( '20300101000000', $watchedItem->getExpiry() ); 1634 $this->assertSame( 0, $watchedItem->getTarget()->getNamespace() ); 1635 } 1636 1637 /** 1638 * @dataProvider provideTestPageFactory 1639 */ 1640 public function testGetWatchedItem_cachedItem( $testPageFactory ) { 1641 $mockDb = $this->getMockDb(); 1642 $mockDb->expects( $this->never() ) 1643 ->method( 'selectRow' ); 1644 1645 $mockUser = new UserIdentityValue( 1, 'MockUser' ); 1646 $linkTarget = $testPageFactory( 100, 0, 'SomeDbKey' ); 1647 $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' ); 1648 1649 $mockCache = $this->getMockCache(); 1650 $mockCache->expects( $this->never() )->method( 'delete' ); 1651 $mockCache->expects( $this->never() )->method( 'set' ); 1652 $mockCache->expects( $this->once() ) 1653 ->method( 'get' ) 1654 ->with( 1655 '0:SomeDbKey:1' 1656 ) 1657 ->willReturn( $cachedItem ); 1658 1659 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1660 1661 $this->assertEquals( 1662 $cachedItem, 1663 $store->getWatchedItem( 1664 $mockUser, 1665 $linkTarget 1666 ) 1667 ); 1668 } 1669 1670 /** 1671 * @dataProvider provideTestPageFactory 1672 */ 1673 public function testGetWatchedItem_noItem( $testPageFactory ) { 1674 $mockDb = $this->getMockDb(); 1675 $mockDb->expects( $this->once() ) 1676 ->method( 'addQuotes' ) 1677 ->willReturn( '20200101000000' ); 1678 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 1679 $mockDb->expects( $this->exactly( 2 ) ) 1680 ->method( 'makeList' ) 1681 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 1682 $mockDb->expects( $this->once() ) 1683 ->method( 'select' ) 1684 ->with( 1685 [ 'watchlist', 'watchlist_expiry' ], 1686 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1687 [ 1688 'wl_user' => 1, 1689 $makeListSql, 1690 'we_expiry IS NULL OR we_expiry > 20200101000000' 1691 ] 1692 ) 1693 ->willReturn( [] ); 1694 1695 $mockCache = $this->getMockCache(); 1696 $mockCache->expects( $this->never() )->method( 'set' ); 1697 $mockCache->expects( $this->never() )->method( 'delete' ); 1698 $mockCache->expects( $this->once() ) 1699 ->method( 'get' ) 1700 ->with( '0:SomeDbKey:1' ) 1701 ->willReturn( false ); 1702 1703 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1704 1705 $this->assertFalse( 1706 $store->getWatchedItem( 1707 new UserIdentityValue( 1, 'MockUser' ), 1708 $testPageFactory( 100, 0, 'SomeDbKey' ) 1709 ) 1710 ); 1711 } 1712 1713 /** 1714 * @dataProvider provideTestPageFactory 1715 */ 1716 public function testGetWatchedItem_anonymousUser( $testPageFactory ) { 1717 $mockDb = $this->getMockDb(); 1718 $mockDb->expects( $this->never() ) 1719 ->method( 'selectRow' ); 1720 1721 $mockCache = $this->getMockCache(); 1722 $mockCache->expects( $this->never() )->method( 'set' ); 1723 $mockCache->expects( $this->never() )->method( 'get' ); 1724 $mockCache->expects( $this->never() )->method( 'delete' ); 1725 1726 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1727 1728 $this->assertFalse( 1729 $store->getWatchedItem( 1730 new UserIdentityValue( 0, 'AnonUser' ), 1731 $testPageFactory( 100, 0, 'SomeDbKey' ) 1732 ) 1733 ); 1734 } 1735 1736 public function testGetWatchedItemsForUser() { 1737 $mockDb = $this->getMockDb(); 1738 $mockDb->expects( $this->once() ) 1739 ->method( 'addQuotes' ) 1740 ->willReturn( '20200101000000' ); 1741 $mockDb->expects( $this->once() ) 1742 ->method( 'select' ) 1743 ->with( 1744 [ 'watchlist', 'watchlist_expiry' ], 1745 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1746 [ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ] 1747 ) 1748 ->willReturn( [ 1749 (object)[ 1750 'wl_namespace' => 0, 1751 'wl_title' => 'Foo1', 1752 'wl_notificationtimestamp' => '20151212010101', 1753 'we_expiry' => '20300101000000' 1754 ], 1755 (object)[ 1756 'wl_namespace' => 1, 1757 'wl_title' => 'Foo2', 1758 'wl_notificationtimestamp' => null, 1759 ], 1760 ] ); 1761 1762 $mockCache = $this->getMockCache(); 1763 $mockCache->expects( $this->never() )->method( 'delete' ); 1764 $mockCache->expects( $this->never() )->method( 'get' ); 1765 $mockCache->expects( $this->never() )->method( 'set' ); 1766 1767 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1768 $user = new UserIdentityValue( 1, 'MockUser' ); 1769 1770 $watchedItems = $store->getWatchedItemsForUser( $user ); 1771 1772 $this->assertIsArray( $watchedItems ); 1773 $this->assertCount( 2, $watchedItems ); 1774 foreach ( $watchedItems as $watchedItem ) { 1775 $this->assertInstanceOf( WatchedItem::class, $watchedItem ); 1776 } 1777 $this->assertEquals( 1778 new WatchedItem( 1779 $user, 1780 new TitleValue( 0, 'Foo1' ), 1781 '20151212010101', 1782 '20300101000000' 1783 ), 1784 $watchedItems[0] 1785 ); 1786 $this->assertEquals( 1787 new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ), 1788 $watchedItems[1] 1789 ); 1790 } 1791 1792 public function provideDbTypes() { 1793 return [ 1794 [ false, DB_REPLICA ], 1795 [ true, DB_PRIMARY ], 1796 ]; 1797 } 1798 1799 /** 1800 * @dataProvider provideDbTypes 1801 */ 1802 public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) { 1803 $mockDb = $this->getMockDb(); 1804 $mockCache = $this->getMockCache(); 1805 $mockLoadBalancer = $this->getMockLBFactory( $mockDb, $dbType ); 1806 $user = new UserIdentityValue( 1, 'MockUser' ); 1807 1808 $mockDb->expects( $this->once() ) 1809 ->method( 'addQuotes' ) 1810 ->willReturn( '20200101000000' ); 1811 $mockDb->expects( $this->once() ) 1812 ->method( 'select' ) 1813 ->with( 1814 [ 'watchlist', 'watchlist_expiry' ], 1815 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1816 [ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ], 1817 $this->isType( 'string' ), 1818 [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ] 1819 ) 1820 ->willReturn( [] ); 1821 1822 $store = $this->newWatchedItemStore( 1823 [ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] ); 1824 1825 $watchedItems = $store->getWatchedItemsForUser( 1826 $user, 1827 [ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ] 1828 ); 1829 $this->assertEquals( [], $watchedItems ); 1830 } 1831 1832 public function testGetWatchedItemsForUser_sortByExpiry() { 1833 $mockDb = $this->getMockDb(); 1834 $mockDb->expects( $this->once() ) 1835 ->method( 'addQuotes' ) 1836 ->willReturn( '20200101000000' ); 1837 $mockDb->expects( $this->once() ) 1838 ->method( 'select' ) 1839 ->with( 1840 [ 'watchlist', 'watchlist_expiry' ], 1841 [ 1842 'wl_namespace', 1843 'wl_title', 1844 'wl_notificationtimestamp', 1845 'we_expiry', 1846 'wl_has_expiry' => null 1847 ], 1848 [ 'wl_user' => 1, 'we_expiry IS NULL OR we_expiry > 20200101000000' ] 1849 ) 1850 ->willReturn( [ 1851 (object)[ 1852 'wl_namespace' => 0, 1853 'wl_title' => 'Foo1', 1854 'wl_notificationtimestamp' => '20151212010101', 1855 'we_expiry' => '20300101000000' 1856 ], 1857 (object)[ 1858 'wl_namespace' => 0, 1859 'wl_title' => 'Foo2', 1860 'wl_notificationtimestamp' => '20151212010101', 1861 'we_expiry' => '20300701000000' 1862 ], 1863 (object)[ 1864 'wl_namespace' => 1, 1865 'wl_title' => 'Foo3', 1866 'wl_notificationtimestamp' => null, 1867 ], 1868 ] ); 1869 1870 $mockCache = $this->getMockCache(); 1871 $mockCache->expects( $this->never() )->method( 'delete' ); 1872 $mockCache->expects( $this->never() )->method( 'get' ); 1873 $mockCache->expects( $this->never() )->method( 'set' ); 1874 1875 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1876 $user = new UserIdentityValue( 1, 'MockUser' ); 1877 1878 $watchedItems = $store->getWatchedItemsForUser( 1879 $user, 1880 [ 'sortByExpiry' => true, 'sort' => WatchedItemStore::SORT_ASC ] 1881 ); 1882 1883 $this->assertIsArray( $watchedItems ); 1884 $this->assertCount( 3, $watchedItems ); 1885 foreach ( $watchedItems as $watchedItem ) { 1886 $this->assertInstanceOf( WatchedItem::class, $watchedItem ); 1887 } 1888 $this->assertEquals( 1889 new WatchedItem( 1890 $user, 1891 new TitleValue( 0, 'Foo1' ), 1892 '20151212010101', 1893 '20300101000000' 1894 ), 1895 $watchedItems[0] 1896 ); 1897 $this->assertEquals( 1898 new WatchedItem( $user, new TitleValue( 1, 'Foo3' ), null ), 1899 $watchedItems[2] 1900 ); 1901 } 1902 1903 public function testGetWatchedItemsForUser_badSortOptionThrowsException() { 1904 $store = $this->newWatchedItemStore(); 1905 1906 $this->expectException( InvalidArgumentException::class ); 1907 $store->getWatchedItemsForUser( 1908 new UserIdentityValue( 1, 'MockUser' ), 1909 [ 'sort' => 'foo' ] 1910 ); 1911 } 1912 1913 /** 1914 * @dataProvider provideTestPageFactory 1915 */ 1916 public function testIsWatchedItem_existingItem( $testPageFactory ) { 1917 $mockDb = $this->getMockDb(); 1918 $mockDb->expects( $this->once() ) 1919 ->method( 'addQuotes' ) 1920 ->willReturn( '20200101000000' ); 1921 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 1922 $mockDb->expects( $this->exactly( 2 ) ) 1923 ->method( 'makeList' ) 1924 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 1925 $mockDb->expects( $this->once() ) 1926 ->method( 'select' ) 1927 ->with( 1928 [ 'watchlist', 'watchlist_expiry' ], 1929 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1930 [ 1931 'wl_user' => 1, 1932 $makeListSql, 1933 'we_expiry IS NULL OR we_expiry > 20200101000000' 1934 ] 1935 ) 1936 ->willReturn( [ 1937 (object)[ 1938 'wl_namespace' => 0, 1939 'wl_title' => 'SomeDbKey', 1940 'wl_notificationtimestamp' => '20151212010101', 1941 ] 1942 ] ); 1943 1944 $mockCache = $this->getMockCache(); 1945 $mockCache->expects( $this->never() )->method( 'delete' ); 1946 $mockCache->expects( $this->once() ) 1947 ->method( 'get' ) 1948 ->with( '0:SomeDbKey:1' ) 1949 ->willReturn( false ); 1950 $mockCache->expects( $this->once() ) 1951 ->method( 'set' ) 1952 ->with( 1953 '0:SomeDbKey:1' 1954 ); 1955 1956 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 1957 1958 $this->assertTrue( 1959 $store->isWatched( 1960 new UserIdentityValue( 1, 'MockUser' ), 1961 $testPageFactory( 100, 0, 'SomeDbKey' ) 1962 ) 1963 ); 1964 } 1965 1966 /** 1967 * @dataProvider provideTestPageFactory 1968 */ 1969 public function testIsWatchedItem_noItem( $testPageFactory ) { 1970 $mockDb = $this->getMockDb(); 1971 $mockDb->expects( $this->once() ) 1972 ->method( 'addQuotes' ) 1973 ->willReturn( '20200101000000' ); 1974 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 1975 $mockDb->expects( $this->exactly( 2 ) ) 1976 ->method( 'makeList' ) 1977 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 1978 $mockDb->expects( $this->once() ) 1979 ->method( 'select' ) 1980 ->with( 1981 [ 'watchlist', 'watchlist_expiry' ], 1982 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 1983 [ 1984 'wl_user' => 1, 1985 $makeListSql, 1986 'we_expiry IS NULL OR we_expiry > 20200101000000' 1987 ] 1988 ) 1989 ->willReturn( [] ); 1990 1991 $mockCache = $this->getMockCache(); 1992 $mockCache->expects( $this->never() )->method( 'set' ); 1993 $mockCache->expects( $this->never() )->method( 'delete' ); 1994 $mockCache->expects( $this->once() ) 1995 ->method( 'get' ) 1996 ->with( '0:SomeDbKey:1' ) 1997 ->willReturn( false ); 1998 1999 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2000 2001 $this->assertFalse( 2002 $store->isWatched( 2003 new UserIdentityValue( 1, 'MockUser' ), 2004 $testPageFactory( 100, 0, 'SomeDbKey' ) 2005 ) 2006 ); 2007 } 2008 2009 /** 2010 * @dataProvider provideTestPageFactory 2011 */ 2012 public function testIsWatchedItem_anonymousUser( $testPageFactory ) { 2013 $mockDb = $this->getMockDb(); 2014 $mockDb->expects( $this->never() ) 2015 ->method( 'selectRow' ); 2016 2017 $mockCache = $this->getMockCache(); 2018 $mockCache->expects( $this->never() )->method( 'set' ); 2019 $mockCache->expects( $this->never() )->method( 'get' ); 2020 $mockCache->expects( $this->never() )->method( 'delete' ); 2021 2022 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2023 2024 $this->assertFalse( 2025 $store->isWatched( 2026 new UserIdentityValue( 0, 'AnonUser' ), 2027 $testPageFactory( 100, 0, 'SomeDbKey' ) 2028 ) 2029 ); 2030 } 2031 2032 /** 2033 * @dataProvider provideTestPageFactory 2034 */ 2035 public function testGetNotificationTimestampsBatch( $testPageFactory ) { 2036 $targets = [ 2037 $testPageFactory( 100, 0, 'SomeDbKey' ), 2038 $testPageFactory( 101, 1, 'AnotherDbKey' ), 2039 ]; 2040 2041 $mockDb = $this->getMockDb(); 2042 $dbResult = [ 2043 (object)[ 2044 'wl_namespace' => '0', 2045 'wl_title' => 'SomeDbKey', 2046 'wl_notificationtimestamp' => '20151212010101', 2047 ], 2048 (object)[ 2049 'wl_namespace' => '1', 2050 'wl_title' => 'AnotherDbKey', 2051 'wl_notificationtimestamp' => null, 2052 ], 2053 ]; 2054 2055 $mockDb->expects( $this->once() ) 2056 ->method( 'makeWhereFrom2d' ) 2057 ->with( 2058 [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], 2059 $this->isType( 'string' ), 2060 $this->isType( 'string' ) 2061 ) 2062 ->willReturn( 'makeWhereFrom2d return value' ); 2063 $mockDb->expects( $this->once() ) 2064 ->method( 'select' ) 2065 ->with( 2066 'watchlist', 2067 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], 2068 [ 2069 'makeWhereFrom2d return value', 2070 'wl_user' => 1 2071 ], 2072 $this->isType( 'string' ) 2073 ) 2074 ->willReturn( $dbResult ); 2075 2076 $mockCache = $this->getMockCache(); 2077 $mockCache->expects( $this->exactly( 2 ) ) 2078 ->method( 'get' ) 2079 ->withConsecutive( 2080 [ '0:SomeDbKey:1' ], 2081 [ '1:AnotherDbKey:1' ] 2082 ) 2083 ->willReturn( null ); 2084 $mockCache->expects( $this->never() )->method( 'set' ); 2085 $mockCache->expects( $this->never() )->method( 'delete' ); 2086 2087 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2088 2089 $this->assertEquals( 2090 [ 2091 0 => [ 'SomeDbKey' => '20151212010101', ], 2092 1 => [ 'AnotherDbKey' => null, ], 2093 ], 2094 $store->getNotificationTimestampsBatch( 2095 new UserIdentityValue( 1, 'MockUser' ), $targets ) 2096 ); 2097 } 2098 2099 /** 2100 * @dataProvider provideTestPageFactory 2101 */ 2102 public function testGetNotificationTimestampsBatch_notWatchedTarget( $testPageFactory ) { 2103 $targets = [ 2104 $testPageFactory( 100, 0, 'OtherDbKey' ), 2105 ]; 2106 2107 $mockDb = $this->getMockDb(); 2108 2109 $mockDb->expects( $this->once() ) 2110 ->method( 'makeWhereFrom2d' ) 2111 ->with( 2112 [ [ 'OtherDbKey' => 1 ] ], 2113 $this->isType( 'string' ), 2114 $this->isType( 'string' ) 2115 ) 2116 ->willReturn( 'makeWhereFrom2d return value' ); 2117 $mockDb->expects( $this->once() ) 2118 ->method( 'select' ) 2119 ->with( 2120 'watchlist', 2121 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], 2122 [ 2123 'makeWhereFrom2d return value', 2124 'wl_user' => 1 2125 ], 2126 $this->isType( 'string' ) 2127 ) 2128 ->willReturn( (object)[] ); 2129 2130 $mockCache = $this->getMockCache(); 2131 $mockCache->expects( $this->once() ) 2132 ->method( 'get' ) 2133 ->with( '0:OtherDbKey:1' ) 2134 ->willReturn( null ); 2135 $mockCache->expects( $this->never() )->method( 'set' ); 2136 $mockCache->expects( $this->never() )->method( 'delete' ); 2137 2138 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2139 2140 $this->assertEquals( 2141 [ 2142 0 => [ 'OtherDbKey' => false, ], 2143 ], 2144 $store->getNotificationTimestampsBatch( 2145 new UserIdentityValue( 1, 'MockUser' ), $targets ) 2146 ); 2147 } 2148 2149 /** 2150 * @dataProvider provideTestPageFactory 2151 */ 2152 public function testGetNotificationTimestampsBatch_cachedItem( $testPageFactory ) { 2153 $targets = [ 2154 $testPageFactory( 100, 0, 'SomeDbKey' ), 2155 $testPageFactory( 101, 1, 'AnotherDbKey' ), 2156 ]; 2157 2158 $user = new UserIdentityValue( 1, 'MockUser' ); 2159 $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' ); 2160 2161 $mockDb = $this->getMockDb(); 2162 2163 $mockDb->expects( $this->once() ) 2164 ->method( 'makeWhereFrom2d' ) 2165 ->with( 2166 [ 1 => [ 'AnotherDbKey' => 1 ] ], 2167 $this->isType( 'string' ), 2168 $this->isType( 'string' ) 2169 ) 2170 ->willReturn( 'makeWhereFrom2d return value' ); 2171 $mockDb->expects( $this->once() ) 2172 ->method( 'select' ) 2173 ->with( 2174 'watchlist', 2175 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], 2176 [ 2177 'makeWhereFrom2d return value', 2178 'wl_user' => 1 2179 ], 2180 $this->isType( 'string' ) 2181 ) 2182 ->willReturn( [ 2183 (object)[ 'wl_namespace' => '1', 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] 2184 ] ); 2185 2186 $mockCache = $this->getMockCache(); 2187 $mockCache->expects( $this->at( 1 ) ) 2188 ->method( 'get' ) 2189 ->with( '0:SomeDbKey:1' ) 2190 ->willReturn( $cachedItem ); 2191 $mockCache->expects( $this->at( 3 ) ) 2192 ->method( 'get' ) 2193 ->with( '1:AnotherDbKey:1' ) 2194 ->willReturn( null ); 2195 $mockCache->expects( $this->never() )->method( 'set' ); 2196 $mockCache->expects( $this->never() )->method( 'delete' ); 2197 2198 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2199 2200 $this->assertEquals( 2201 [ 2202 0 => [ 'SomeDbKey' => '20151212010101', ], 2203 1 => [ 'AnotherDbKey' => null, ], 2204 ], 2205 $store->getNotificationTimestampsBatch( $user, $targets ) 2206 ); 2207 } 2208 2209 /** 2210 * @dataProvider provideTestPageFactory 2211 */ 2212 public function testGetNotificationTimestampsBatch_allItemsCached( $testPageFactory ) { 2213 $targets = [ 2214 $testPageFactory( 100, 0, 'SomeDbKey' ), 2215 $testPageFactory( 101, 1, 'AnotherDbKey' ), 2216 ]; 2217 2218 $user = new UserIdentityValue( 1, 'MockUser' ); 2219 $cachedItems = [ 2220 new WatchedItem( $user, $targets[0], '20151212010101' ), 2221 new WatchedItem( $user, $targets[1], null ), 2222 ]; 2223 $mockDb = $this->createNoOpMock( IDatabase::class ); 2224 2225 $mockCache = $this->getMockCache(); 2226 $mockCache->expects( $this->at( 1 ) ) 2227 ->method( 'get' ) 2228 ->with( '0:SomeDbKey:1' ) 2229 ->willReturn( $cachedItems[0] ); 2230 $mockCache->expects( $this->at( 3 ) ) 2231 ->method( 'get' ) 2232 ->with( '1:AnotherDbKey:1' ) 2233 ->willReturn( $cachedItems[1] ); 2234 $mockCache->expects( $this->never() )->method( 'set' ); 2235 $mockCache->expects( $this->never() )->method( 'delete' ); 2236 2237 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2238 2239 $this->assertEquals( 2240 [ 2241 0 => [ 'SomeDbKey' => '20151212010101', ], 2242 1 => [ 'AnotherDbKey' => null, ], 2243 ], 2244 $store->getNotificationTimestampsBatch( $user, $targets ) 2245 ); 2246 } 2247 2248 /** 2249 * @dataProvider provideTestPageFactory 2250 */ 2251 public function testGetNotificationTimestampsBatch_anonymousUser( $testPageFactory ) { 2252 $targets = [ 2253 $testPageFactory( 100, 0, 'SomeDbKey' ), 2254 $testPageFactory( 101, 1, 'AnotherDbKey' ), 2255 ]; 2256 2257 $mockDb = $this->createNoOpMock( IDatabase::class ); 2258 2259 $mockCache = $this->createNoOpMock( HashBagOStuff::class ); 2260 2261 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2262 2263 $this->assertEquals( 2264 [ 2265 0 => [ 'SomeDbKey' => false, ], 2266 1 => [ 'AnotherDbKey' => false, ], 2267 ], 2268 $store->getNotificationTimestampsBatch( 2269 new UserIdentityValue( 0, 'AnonUser' ), $targets ) 2270 ); 2271 } 2272 2273 /** 2274 * @dataProvider provideTestPageFactory 2275 */ 2276 public function testResetNotificationTimestamp_anonymousUser( $testPageFactory ) { 2277 $mockDb = $this->getMockDb(); 2278 $mockDb->expects( $this->never() ) 2279 ->method( 'selectRow' ); 2280 2281 $mockCache = $this->getMockCache(); 2282 $mockCache->expects( $this->never() )->method( 'get' ); 2283 $mockCache->expects( $this->never() )->method( 'set' ); 2284 $mockCache->expects( $this->never() )->method( 'delete' ); 2285 2286 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 2287 2288 $this->assertFalse( 2289 $store->resetNotificationTimestamp( 2290 new UserIdentityValue( 0, 'AnonUser' ), 2291 $testPageFactory( 100, 0, 'SomeDbKey' ) 2292 ) 2293 ); 2294 } 2295 2296 /** 2297 * @dataProvider provideTestPageFactory 2298 */ 2299 public function testResetNotificationTimestamp_noItem( $testPageFactory ) { 2300 $mockDb = $this->getMockDb(); 2301 $mockDb->expects( $this->once() ) 2302 ->method( 'addQuotes' ) 2303 ->willReturn( '20200101000000' ); 2304 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2305 $mockDb->expects( $this->exactly( 2 ) ) 2306 ->method( 'makeList' ) 2307 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2308 $mockDb->expects( $this->once() ) 2309 ->method( 'select' ) 2310 ->with( 2311 [ 'watchlist', 'watchlist_expiry' ], 2312 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2313 [ 2314 'wl_user' => 1, 2315 $makeListSql, 2316 'we_expiry IS NULL OR we_expiry > 20200101000000' 2317 ] 2318 ) 2319 ->willReturn( [] ); 2320 2321 $mockCache = $this->getMockCache(); 2322 $mockCache->expects( $this->never() )->method( 'get' ); 2323 $mockCache->expects( $this->never() )->method( 'set' ); 2324 $mockCache->expects( $this->never() )->method( 'delete' ); 2325 2326 $user = new UserIdentityValue( 1, 'MockUser' ); 2327 2328 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2329 $titleFactory = $this->getTitleFactory( $title ); 2330 2331 $store = $this->newWatchedItemStore( [ 2332 'db' => $mockDb, 2333 'cache' => $mockCache, 2334 'titleFactory' => $titleFactory, 2335 ] ); 2336 2337 $this->assertFalse( 2338 $store->resetNotificationTimestamp( 2339 $user, 2340 $title 2341 ) 2342 ); 2343 } 2344 2345 /** 2346 * @dataProvider provideTestPageFactory 2347 */ 2348 public function testResetNotificationTimestamp_item( $testPageFactory ) { 2349 $user = new UserIdentityValue( 1, 'MockUser' ); 2350 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2351 2352 $mockDb = $this->getMockDb(); 2353 $mockDb->expects( $this->once() ) 2354 ->method( 'addQuotes' ) 2355 ->willReturn( '20200101000000' ); 2356 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2357 $mockDb->expects( $this->exactly( 2 ) ) 2358 ->method( 'makeList' ) 2359 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2360 $mockDb->expects( $this->once() ) 2361 ->method( 'select' ) 2362 ->with( 2363 [ 'watchlist', 'watchlist_expiry' ], 2364 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2365 [ 2366 'wl_user' => 1, 2367 $makeListSql, 2368 'we_expiry IS NULL OR we_expiry > 20200101000000' 2369 ] 2370 ) 2371 ->willReturn( [ 2372 (object)[ 2373 'wl_namespace' => 0, 2374 'wl_title' => 'SomeDbKey', 2375 'wl_notificationtimestamp' => '20151212010101', 2376 ] 2377 ] ); 2378 2379 $mockCache = $this->getMockCache(); 2380 $mockCache->expects( $this->never() )->method( 'get' ); 2381 $mockCache->expects( $this->once() ) 2382 ->method( 'set' ) 2383 ->with( 2384 '0:SomeDbKey:1', 2385 $this->isInstanceOf( WatchedItem::class ) 2386 ); 2387 $mockCache->expects( $this->once() ) 2388 ->method( 'delete' ) 2389 ->with( '0:SomeDbKey:1' ); 2390 2391 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2392 $mockQueueGroup->expects( $this->once() ) 2393 ->method( 'lazyPush' ) 2394 ->willReturnCallback( static function ( ActivityUpdateJob $job ) { 2395 // don't run 2396 } ); 2397 2398 // We don't care if these methods actually do anything here 2399 $mockRevisionLookup = $this->getMockRevisionLookup( [ 2400 'getRevisionByTitle' => static function () { 2401 return null; 2402 }, 2403 'getTimestampFromId' => static function () { 2404 return '00000000000000'; 2405 }, 2406 ] ); 2407 2408 $titleFactory = $this->getTitleFactory( $title ); 2409 2410 $store = $this->newWatchedItemStore( [ 2411 'db' => $mockDb, 2412 'queueGroup' => $mockQueueGroup, 2413 'cache' => $mockCache, 2414 'revisionLookup' => $mockRevisionLookup, 2415 'titleFactory' => $titleFactory, 2416 ] ); 2417 2418 $this->assertTrue( 2419 $store->resetNotificationTimestamp( 2420 $user, 2421 $title 2422 ) 2423 ); 2424 } 2425 2426 /** 2427 * @dataProvider provideTestPageFactory 2428 */ 2429 public function testResetNotificationTimestamp_noItemForced( $testPageFactory ) { 2430 $user = new UserIdentityValue( 1, 'MockUser' ); 2431 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2432 2433 $mockDb = $this->getMockDb(); 2434 $mockDb->expects( $this->never() ) 2435 ->method( 'selectRow' ); 2436 2437 $mockCache = $this->getMockCache(); 2438 $mockCache->expects( $this->never() )->method( 'get' ); 2439 $mockCache->expects( $this->never() )->method( 'set' ); 2440 $mockCache->expects( $this->once() ) 2441 ->method( 'delete' ) 2442 ->with( '0:SomeDbKey:1' ); 2443 2444 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2445 2446 // We don't care if these methods actually do anything here 2447 $mockRevisionLookup = $this->getMockRevisionLookup( [ 2448 'getRevisionByTitle' => static function () { 2449 return null; 2450 }, 2451 'getTimestampFromId' => static function () { 2452 return '00000000000000'; 2453 }, 2454 ] ); 2455 2456 $titleFactory = $this->getTitleFactory( $title ); 2457 2458 $store = $this->newWatchedItemStore( [ 2459 'db' => $mockDb, 2460 'queueGroup' => $mockQueueGroup, 2461 'cache' => $mockCache, 2462 'revisionLookup' => $mockRevisionLookup, 2463 'titleFactory' => $titleFactory, 2464 ] ); 2465 2466 $mockQueueGroup->method( 'lazyPush' ) 2467 ->willReturnCallback( static function ( ActivityUpdateJob $job ) { 2468 // don't run 2469 } ); 2470 2471 $this->assertTrue( 2472 $store->resetNotificationTimestamp( 2473 $user, 2474 $title, 2475 'force' 2476 ) 2477 ); 2478 } 2479 2480 /** 2481 * @param ActivityUpdateJob $job 2482 * @param LinkTarget|PageIdentity $expectedTitle 2483 * @param string $expectedUserId 2484 * @param callable $notificationTimestampCondition 2485 */ 2486 private function verifyCallbackJob( 2487 ActivityUpdateJob $job, 2488 $expectedTitle, 2489 $expectedUserId, 2490 callable $notificationTimestampCondition 2491 ) { 2492 $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() ); 2493 $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() ); 2494 2495 $jobParams = $job->getParams(); 2496 $this->assertArrayHasKey( 'type', $jobParams ); 2497 $this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] ); 2498 $this->assertArrayHasKey( 'userid', $jobParams ); 2499 $this->assertEquals( $expectedUserId, $jobParams['userid'] ); 2500 $this->assertArrayHasKey( 'notifTime', $jobParams ); 2501 $this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) ); 2502 } 2503 2504 /** 2505 * @dataProvider provideTestPageFactory 2506 */ 2507 public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced( $testPageFactory ) { 2508 $user = new UserIdentityValue( 1, 'MockUser' ); 2509 $oldid = 22; 2510 $title = $testPageFactory( 100, 0, 'SomeTitle' ); 2511 2512 $mockDb = $this->getMockDb(); 2513 $mockDb->expects( $this->never() ) 2514 ->method( 'selectRow' ); 2515 2516 $mockCache = $this->getMockCache(); 2517 $mockCache->expects( $this->never() )->method( 'get' ); 2518 $mockCache->expects( $this->never() )->method( 'set' ); 2519 $mockCache->expects( $this->once() ) 2520 ->method( 'delete' ) 2521 ->with( '0:SomeTitle:1' ); 2522 2523 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2524 2525 $mockRevisionRecord = $this->createNoOpMock( RevisionRecord::class ); 2526 2527 $mockRevisionLookup = $this->getMockRevisionLookup( [ 2528 'getTimestampFromId' => static function () { 2529 return '00000000000000'; 2530 }, 2531 'getRevisionById' => function ( $id, $flags ) use ( $oldid, $mockRevisionRecord ) { 2532 $this->assertSame( $oldid, $id ); 2533 $this->assertSame( 0, $flags ); 2534 return $mockRevisionRecord; 2535 }, 2536 'getNextRevision' => 2537 function ( $oldRev ) use ( $mockRevisionRecord ) { 2538 $this->assertSame( $mockRevisionRecord, $oldRev ); 2539 return false; 2540 }, 2541 ], [ 2542 'getNextRevision' => 1, 2543 ] ); 2544 2545 $titleFactory = $this->getTitleFactory( $title ); 2546 2547 $store = $this->newWatchedItemStore( [ 2548 'db' => $mockDb, 2549 'queueGroup' => $mockQueueGroup, 2550 'cache' => $mockCache, 2551 'revisionLookup' => $mockRevisionLookup, 2552 'titleFactory' => $titleFactory, 2553 ] ); 2554 2555 $mockQueueGroup->method( 'lazyPush' ) 2556 ->willReturnCallback( 2557 function ( ActivityUpdateJob $job ) use ( $title, $user ) { 2558 $this->verifyCallbackJob( 2559 $job, 2560 $title, 2561 $user->getId(), 2562 static function ( $time ) { 2563 return $time === null; 2564 } 2565 ); 2566 } 2567 ); 2568 2569 $this->assertTrue( 2570 $store->resetNotificationTimestamp( 2571 $user, 2572 $title, 2573 'force', 2574 $oldid 2575 ) 2576 ); 2577 } 2578 2579 /** 2580 * @dataProvider provideTestPageFactory 2581 */ 2582 public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced( $testPageFactory ) { 2583 $user = new UserIdentityValue( 1, 'MockUser' ); 2584 $oldid = 22; 2585 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2586 2587 $mockRevision = $this->createNoOpMock( RevisionRecord::class ); 2588 $mockNextRevision = $this->createNoOpMock( RevisionRecord::class ); 2589 2590 $mockDb = $this->getMockDb(); 2591 $mockDb->expects( $this->once() ) 2592 ->method( 'addQuotes' ) 2593 ->willReturn( '20200101000000' ); 2594 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2595 $mockDb->expects( $this->exactly( 2 ) ) 2596 ->method( 'makeList' ) 2597 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2598 $mockDb->expects( $this->once() ) 2599 ->method( 'select' ) 2600 ->with( 2601 [ 'watchlist', 'watchlist_expiry' ], 2602 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2603 [ 2604 'wl_user' => 1, 2605 $makeListSql, 2606 'we_expiry IS NULL OR we_expiry > 20200101000000' 2607 ] 2608 ) 2609 ->willReturn( [ 2610 (object)[ 2611 'wl_namespace' => 0, 2612 'wl_title' => 'SomeDbKey', 2613 'wl_notificationtimestamp' => '20151212010101', 2614 ] 2615 ] ); 2616 2617 $mockCache = $this->getMockCache(); 2618 $mockCache->expects( $this->never() )->method( 'get' ); 2619 $mockCache->expects( $this->once() ) 2620 ->method( 'set' ) 2621 ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); 2622 $mockCache->expects( $this->once() ) 2623 ->method( 'delete' ) 2624 ->with( '0:SomeDbKey:1' ); 2625 2626 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2627 2628 $mockRevisionLookup = $this->getMockRevisionLookup( 2629 [ 2630 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { 2631 $this->assertSame( $oldid, $oldidParam ); 2632 }, 2633 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { 2634 $this->assertSame( $oldid, $id ); 2635 return $mockRevision; 2636 }, 2637 'getNextRevision' => 2638 function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { 2639 $this->assertSame( $mockRevision, $rev ); 2640 return $mockNextRevision; 2641 }, 2642 ], 2643 [ 2644 'getTimestampFromId' => 2, 2645 'getRevisionById' => 1, 2646 'getNextRevision' => 1, 2647 ] 2648 ); 2649 2650 $titleFactory = $this->getTitleFactory( $title ); 2651 2652 $store = $this->newWatchedItemStore( [ 2653 'db' => $mockDb, 2654 'queueGroup' => $mockQueueGroup, 2655 'cache' => $mockCache, 2656 'revisionLookup' => $mockRevisionLookup, 2657 'titleFactory' => $titleFactory, 2658 ] ); 2659 2660 $mockQueueGroup->method( 'lazyPush' ) 2661 ->willReturnCallback( 2662 function ( ActivityUpdateJob $job ) use ( $title, $user ) { 2663 $this->verifyCallbackJob( 2664 $job, 2665 $title, 2666 $user->getId(), 2667 static function ( $time ) { 2668 return $time !== null && $time > '20151212010101'; 2669 } 2670 ); 2671 } 2672 ); 2673 2674 $this->assertTrue( 2675 $store->resetNotificationTimestamp( 2676 $user, 2677 $title, 2678 'force', 2679 $oldid 2680 ) 2681 ); 2682 } 2683 2684 /** 2685 * @dataProvider provideTestPageFactory 2686 */ 2687 public function testResetNotificationTimestamp_notWatchedPageForced( $testPageFactory ) { 2688 $user = new UserIdentityValue( 1, 'MockUser' ); 2689 $oldid = 22; 2690 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2691 2692 $mockDb = $this->getMockDb(); 2693 $mockDb->expects( $this->once() ) 2694 ->method( 'addQuotes' ) 2695 ->willReturn( '20200101000000' ); 2696 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2697 $mockDb->expects( $this->exactly( 2 ) ) 2698 ->method( 'makeList' ) 2699 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2700 $mockDb->expects( $this->once() ) 2701 ->method( 'select' ) 2702 ->with( 2703 [ 'watchlist', 'watchlist_expiry' ], 2704 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2705 [ 2706 'wl_user' => 1, 2707 $makeListSql, 2708 'we_expiry IS NULL OR we_expiry > 20200101000000' 2709 ] 2710 ) 2711 ->willReturn( false ); 2712 2713 $mockCache = $this->getMockCache(); 2714 $mockCache->expects( $this->never() )->method( 'get' ); 2715 $mockCache->expects( $this->never() )->method( 'set' ); 2716 $mockCache->expects( $this->once() ) 2717 ->method( 'delete' ) 2718 ->with( '0:SomeDbKey:1' ); 2719 2720 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2721 2722 $mockRevision = $this->createNoOpMock( RevisionRecord::class ); 2723 $mockNextRevision = $this->createNoOpMock( RevisionRecord::class ); 2724 2725 $mockRevisionLookup = $this->getMockRevisionLookup( 2726 [ 2727 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { 2728 $this->assertSame( $oldid, $oldidParam ); 2729 }, 2730 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { 2731 $this->assertSame( $oldid, $id ); 2732 return $mockRevision; 2733 }, 2734 'getNextRevision' => 2735 function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { 2736 $this->assertSame( $mockRevision, $rev ); 2737 return $mockNextRevision; 2738 }, 2739 ], 2740 [ 2741 'getTimestampFromId' => 1, 2742 'getRevisionById' => 1, 2743 'getNextRevision' => 1, 2744 ] 2745 ); 2746 2747 $titleFactory = $this->getTitleFactory( $title ); 2748 2749 $store = $this->newWatchedItemStore( [ 2750 'db' => $mockDb, 2751 'queueGroup' => $mockQueueGroup, 2752 'cache' => $mockCache, 2753 'revisionLookup' => $mockRevisionLookup, 2754 'titleFactory' => $titleFactory, 2755 ] ); 2756 2757 $mockQueueGroup->method( 'lazyPush' ) 2758 ->willReturnCallback( 2759 function ( ActivityUpdateJob $job ) use ( $title, $user ) { 2760 $this->verifyCallbackJob( 2761 $job, 2762 $title, 2763 $user->getId(), 2764 static function ( $time ) { 2765 return $time === null; 2766 } 2767 ); 2768 } 2769 ); 2770 2771 $this->assertTrue( 2772 $store->resetNotificationTimestamp( 2773 $user, 2774 $title, 2775 'force', 2776 $oldid 2777 ) 2778 ); 2779 } 2780 2781 /** 2782 * @dataProvider provideTestPageFactory 2783 */ 2784 public function testResetNotificationTimestamp_futureNotificationTimestampForced( $testPageFactory ) { 2785 $user = new UserIdentityValue( 1, 'MockUser' ); 2786 $oldid = 22; 2787 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2788 2789 $mockDb = $this->getMockDb(); 2790 $mockDb->expects( $this->once() ) 2791 ->method( 'addQuotes' ) 2792 ->willReturn( '20200101000000' ); 2793 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2794 $mockDb->expects( $this->exactly( 2 ) ) 2795 ->method( 'makeList' ) 2796 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2797 $mockDb->expects( $this->once() ) 2798 ->method( 'select' ) 2799 ->with( 2800 [ 'watchlist', 'watchlist_expiry' ], 2801 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2802 [ 2803 'wl_user' => 1, 2804 $makeListSql, 2805 'we_expiry IS NULL OR we_expiry > 20200101000000' 2806 ] 2807 ) 2808 ->willReturn( [ 2809 (object)[ 2810 'wl_namespace' => 0, 2811 'wl_title' => 'SomeDbKey', 2812 'wl_notificationtimestamp' => '30151212010101', 2813 ] 2814 ] ); 2815 2816 $mockCache = $this->getMockCache(); 2817 $mockCache->expects( $this->never() )->method( 'get' ); 2818 $mockCache->expects( $this->once() ) 2819 ->method( 'set' ) 2820 ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); 2821 $mockCache->expects( $this->once() ) 2822 ->method( 'delete' ) 2823 ->with( '0:SomeDbKey:1' ); 2824 2825 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2826 2827 $mockRevision = $this->createNoOpMock( RevisionRecord::class ); 2828 $mockNextRevision = $this->createNoOpMock( RevisionRecord::class ); 2829 2830 $mockRevisionLookup = $this->getMockRevisionLookup( 2831 [ 2832 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { 2833 $this->assertEquals( $oldid, $oldidParam ); 2834 }, 2835 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { 2836 $this->assertSame( $oldid, $id ); 2837 return $mockRevision; 2838 }, 2839 'getNextRevision' => 2840 function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { 2841 $this->assertSame( $mockRevision, $rev ); 2842 return $mockNextRevision; 2843 }, 2844 ], 2845 [ 2846 'getTimestampFromId' => 2, 2847 'getRevisionById' => 1, 2848 'getNextRevision' => 1, 2849 ] 2850 ); 2851 2852 $titleFactory = $this->getTitleFactory( $title ); 2853 2854 $store = $this->newWatchedItemStore( [ 2855 'db' => $mockDb, 2856 'queueGroup' => $mockQueueGroup, 2857 'cache' => $mockCache, 2858 'revisionLookup' => $mockRevisionLookup, 2859 'titleFactory' => $titleFactory, 2860 ] ); 2861 2862 $mockQueueGroup->method( 'lazyPush' ) 2863 ->willReturnCallback( 2864 function ( ActivityUpdateJob $job ) use ( $title, $user ) { 2865 $this->verifyCallbackJob( 2866 $job, 2867 $title, 2868 $user->getId(), 2869 static function ( $time ) { 2870 return $time === '30151212010101'; 2871 } 2872 ); 2873 } 2874 ); 2875 2876 $this->assertTrue( 2877 $store->resetNotificationTimestamp( 2878 $user, 2879 $title, 2880 'force', 2881 $oldid 2882 ) 2883 ); 2884 } 2885 2886 /** 2887 * @dataProvider provideTestPageFactory 2888 */ 2889 public function testResetNotificationTimestamp_futureNotificationTimestampNotForced( $testPageFactory ) { 2890 $user = new UserIdentityValue( 1, 'MockUser' ); 2891 $oldid = 22; 2892 $title = $testPageFactory( 100, 0, 'SomeDbKey' ); 2893 2894 $mockDb = $this->getMockDb(); 2895 $mockDb->expects( $this->once() ) 2896 ->method( 'addQuotes' ) 2897 ->willReturn( '20200101000000' ); 2898 $makeListSql = "wl_namespace = 0 AND wl_title = 'SomeDbKey'"; 2899 $mockDb->expects( $this->exactly( 2 ) ) 2900 ->method( 'makeList' ) 2901 ->willReturnOnConsecutiveCalls( $makeListSql, $makeListSql ); 2902 $mockDb->expects( $this->once() ) 2903 ->method( 'select' ) 2904 ->with( 2905 [ 'watchlist', 'watchlist_expiry' ], 2906 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp', 'we_expiry' ], 2907 [ 2908 'wl_user' => 1, 2909 $makeListSql, 2910 'we_expiry IS NULL OR we_expiry > 20200101000000', 2911 ] 2912 ) 2913 ->willReturn( [ 2914 (object)[ 2915 'wl_namespace' => 0, 2916 'wl_title' => 'SomeDbKey', 2917 'wl_notificationtimestamp' => '30151212010101', 2918 ] 2919 ] ); 2920 2921 $mockCache = $this->getMockCache(); 2922 $mockCache->expects( $this->never() )->method( 'get' ); 2923 $mockCache->expects( $this->once() ) 2924 ->method( 'set' ) 2925 ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); 2926 $mockCache->expects( $this->once() ) 2927 ->method( 'delete' ) 2928 ->with( '0:SomeDbKey:1' ); 2929 2930 $mockQueueGroup = $this->getMockJobQueueGroup( false ); 2931 2932 $mockRevision = $this->createNoOpMock( RevisionRecord::class ); 2933 $mockNextRevision = $this->createNoOpMock( RevisionRecord::class ); 2934 2935 $mockRevisionLookup = $this->getMockRevisionLookup( 2936 [ 2937 'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) { 2938 $this->assertEquals( $oldid, $oldidParam ); 2939 }, 2940 'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) { 2941 $this->assertSame( $oldid, $id ); 2942 return $mockRevision; 2943 }, 2944 'getNextRevision' => 2945 function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) { 2946 $this->assertSame( $mockRevision, $rev ); 2947 return $mockNextRevision; 2948 }, 2949 ], 2950 [ 2951 'getTimestampFromId' => 2, 2952 'getRevisionById' => 1, 2953 'getNextRevision' => 1, 2954 ] 2955 ); 2956 2957 $titleFactory = $this->getTitleFactory( $title ); 2958 2959 $store = $this->newWatchedItemStore( [ 2960 'db' => $mockDb, 2961 'queueGroup' => $mockQueueGroup, 2962 'cache' => $mockCache, 2963 'revisionLookup' => $mockRevisionLookup, 2964 'titleFactory' => $titleFactory, 2965 ] ); 2966 2967 $mockQueueGroup->method( 'lazyPush' ) 2968 ->willReturnCallback( 2969 function ( ActivityUpdateJob $job ) use ( $title, $user ) { 2970 $this->verifyCallbackJob( 2971 $job, 2972 $title, 2973 $user->getId(), 2974 static function ( $time ) { 2975 return $time === false; 2976 } 2977 ); 2978 } 2979 ); 2980 2981 $this->assertTrue( 2982 $store->resetNotificationTimestamp( 2983 $user, 2984 $title, 2985 '', 2986 $oldid 2987 ) 2988 ); 2989 } 2990 2991 public function testSetNotificationTimestampsForUser_anonUser() { 2992 $store = $this->newWatchedItemStore(); 2993 $this->assertFalse( $store->setNotificationTimestampsForUser( 2994 new UserIdentityValue( 0, 'AnonUser' ), '' ) ); 2995 } 2996 2997 public function testSetNotificationTimestampsForUser_allRows() { 2998 $user = new UserIdentityValue( 1, 'MockUser' ); 2999 $timestamp = '20100101010101'; 3000 3001 $store = $this->newWatchedItemStore(); 3002 3003 // Note: This does not actually assert the job is correct 3004 $callableCallCounter = 0; 3005 $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { 3006 $callableCallCounter++; 3007 $this->assertIsCallable( $callable ); 3008 }; 3009 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); 3010 3011 $this->assertTrue( 3012 $store->setNotificationTimestampsForUser( $user, $timestamp ) 3013 ); 3014 $this->assertSame( 1, $callableCallCounter ); 3015 } 3016 3017 public function testSetNotificationTimestampsForUser_nullTimestamp() { 3018 $user = new UserIdentityValue( 1, 'MockUser' ); 3019 $timestamp = null; 3020 3021 $store = $this->newWatchedItemStore(); 3022 3023 // Note: This does not actually assert the job is correct 3024 $callableCallCounter = 0; 3025 $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { 3026 $callableCallCounter++; 3027 $this->assertIsCallable( $callable ); 3028 }; 3029 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); 3030 3031 $this->assertTrue( 3032 $store->setNotificationTimestampsForUser( $user, $timestamp ) 3033 ); 3034 } 3035 3036 /** 3037 * @dataProvider provideTestPageFactory 3038 */ 3039 public function testSetNotificationTimestampsForUser_specificTargets( $testPageFactory ) { 3040 $user = new UserIdentityValue( 1, 'MockUser' ); 3041 $timestamp = '20100101010101'; 3042 $targets = [ $testPageFactory( 100, 0, 'Foo' ), $testPageFactory( 101, 0, 'Bar' ) ]; 3043 3044 $mockDb = $this->getMockDb(); 3045 $mockDb->expects( $this->once() ) 3046 ->method( 'update' ) 3047 ->with( 3048 'watchlist', 3049 [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ], 3050 [ 'wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => [ 'Foo', 'Bar' ] ] 3051 ) 3052 ->willReturn( true ); 3053 $mockDb->expects( $this->once() ) 3054 ->method( 'timestamp' ) 3055 ->willReturnCallback( static function ( $value ) { 3056 return 'TS' . $value . 'TS'; 3057 } ); 3058 $mockDb->expects( $this->once() ) 3059 ->method( 'affectedRows' ) 3060 ->willReturn( 2 ); 3061 3062 $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] ); 3063 3064 $this->assertTrue( 3065 $store->setNotificationTimestampsForUser( $user, $timestamp, $targets ) 3066 ); 3067 } 3068 3069 /** 3070 * @dataProvider provideTestPageFactory 3071 */ 3072 public function testUpdateNotificationTimestamp_watchersExist( $testPageFactory ) { 3073 $mockDb = $this->getMockDb(); 3074 $mockDb->expects( $this->once() ) 3075 ->method( 'addQuotes' ) 3076 ->willReturn( '20200101000000' ); 3077 $mockDb->expects( $this->once() ) 3078 ->method( 'selectFieldValues' ) 3079 ->with( 3080 [ 'watchlist', 'watchlist_expiry' ], 3081 'wl_user', 3082 [ 3083 'wl_user != 1', 3084 'wl_namespace' => 0, 3085 'wl_title' => 'SomeDbKey', 3086 'wl_notificationtimestamp IS NULL', 3087 'we_expiry IS NULL OR we_expiry > 20200101000000', 3088 ] 3089 ) 3090 ->willReturn( [ '2', '3' ] ); 3091 $mockDb->expects( $this->once() ) 3092 ->method( 'update' ) 3093 ->with( 3094 'watchlist', 3095 [ 'wl_notificationtimestamp' => null ], 3096 [ 3097 'wl_user' => [ 2, 3 ], 3098 'wl_namespace' => 0, 3099 'wl_title' => 'SomeDbKey', 3100 ] 3101 ); 3102 3103 $mockCache = $this->getMockCache(); 3104 $mockCache->expects( $this->never() )->method( 'set' ); 3105 $mockCache->expects( $this->never() )->method( 'get' ); 3106 $mockCache->expects( $this->never() )->method( 'delete' ); 3107 3108 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 3109 3110 // updateNotificationTimestamp calls DeferredUpdates::addCallableUpdate 3111 // in normal operation, but we want to test that update actually running, so 3112 // override it 3113 $mockCallback = function ( $callable, $stage, $dbw ) use ( $mockDb ) { 3114 $this->assertIsCallable( $callable ); 3115 $this->assertSame( DeferredUpdates::POSTSEND, $stage ); 3116 $this->assertSame( $mockDb, $dbw ); 3117 ( $callable )(); 3118 }; 3119 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); 3120 3121 $this->assertEquals( 3122 [ 2, 3 ], 3123 $store->updateNotificationTimestamp( 3124 new UserIdentityValue( 1, 'MockUser' ), 3125 $testPageFactory( 100, 0, 'SomeDbKey' ), 3126 '20151212010101' 3127 ) 3128 ); 3129 } 3130 3131 /** 3132 * @dataProvider provideTestPageFactory 3133 */ 3134 public function testUpdateNotificationTimestamp_noWatchers( $testPageFactory ) { 3135 $mockDb = $this->getMockDb(); 3136 $mockDb->expects( $this->once() ) 3137 ->method( 'addQuotes' ) 3138 ->willReturn( '20200101000000' ); 3139 $mockDb->expects( $this->once() ) 3140 ->method( 'selectFieldValues' ) 3141 ->with( 3142 [ 'watchlist', 'watchlist_expiry' ], 3143 'wl_user', 3144 [ 3145 'wl_user != 1', 3146 'wl_namespace' => 0, 3147 'wl_title' => 'SomeDbKey', 3148 'wl_notificationtimestamp IS NULL', 3149 'we_expiry IS NULL OR we_expiry > 20200101000000', 3150 ], 3151 'WatchedItemStore::updateNotificationTimestamp', 3152 [], 3153 [ 'watchlist_expiry' => [ 'LEFT JOIN', 'wl_id = we_item' ] ] 3154 ) 3155 ->willReturn( [] ); 3156 $mockDb->expects( $this->never() ) 3157 ->method( 'update' ); 3158 3159 $mockCache = $this->getMockCache(); 3160 $mockCache->expects( $this->never() )->method( 'set' ); 3161 $mockCache->expects( $this->never() )->method( 'get' ); 3162 $mockCache->expects( $this->never() )->method( 'delete' ); 3163 3164 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 3165 3166 $watchers = $store->updateNotificationTimestamp( 3167 new UserIdentityValue( 1, 'MockUser' ), 3168 $testPageFactory( 100, 0, 'SomeDbKey' ), 3169 '20151212010101' 3170 ); 3171 $this->assertSame( [], $watchers ); 3172 } 3173 3174 /** 3175 * @dataProvider provideTestPageFactory 3176 */ 3177 public function testUpdateNotificationTimestamp_clearsCachedItems( $testPageFactory ) { 3178 $user = new UserIdentityValue( 1, 'MockUser' ); 3179 $titleValue = $testPageFactory( 100, 0, 'SomeDbKey' ); 3180 3181 $mockDb = $this->getMockDb(); 3182 $mockDb->expects( $this->once() ) 3183 ->method( 'select' ) 3184 ->willReturn( [ 3185 (object)[ 3186 'wl_namespace' => 0, 3187 'wl_title' => 'SomeDbKey', 3188 'wl_notificationtimestamp' => '20151212010101' 3189 ] 3190 ] ); 3191 $mockDb->expects( $this->once() ) 3192 ->method( 'selectFieldValues' ) 3193 ->willReturn( [ '2', '3' ] ); 3194 $mockDb->expects( $this->once() ) 3195 ->method( 'update' ); 3196 3197 $mockCache = $this->getMockCache(); 3198 $mockCache->expects( $this->once() ) 3199 ->method( 'set' ) 3200 ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); 3201 $mockCache->expects( $this->once() ) 3202 ->method( 'get' ) 3203 ->with( '0:SomeDbKey:1' ); 3204 $mockCache->expects( $this->once() ) 3205 ->method( 'delete' ) 3206 ->with( '0:SomeDbKey:1' ); 3207 3208 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 3209 3210 // This will add the item to the cache 3211 $store->getWatchedItem( $user, $titleValue ); 3212 3213 // updateNotificationTimestamp calls DeferredUpdates::addCallableUpdate 3214 // in normal operation, but we want to test that update actually running, so 3215 // override it 3216 $mockCallback = function ( $callable, $stage, $dbw ) use ( $mockDb ) { 3217 $this->assertIsCallable( $callable ); 3218 $this->assertSame( DeferredUpdates::POSTSEND, $stage ); 3219 $this->assertSame( $mockDb, $dbw ); 3220 ( $callable )(); 3221 }; 3222 $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); 3223 3224 $store->updateNotificationTimestamp( 3225 new UserIdentityValue( 1, 'MockUser' ), 3226 $titleValue, 3227 '20151212010101' 3228 ); 3229 } 3230 3231 public function testRemoveExpired() { 3232 $mockDb = $this->getMockDb(); 3233 3234 // addQuotes is used for the expiry value. 3235 $mockDb->expects( $this->once() ) 3236 ->method( 'addQuotes' ) 3237 ->willReturn( '20200101000000' ); 3238 3239 // Select watchlist IDs. 3240 $mockDb->expects( $this->exactly( 2 ) ) 3241 ->method( 'selectFieldValues' ) 3242 ->withConsecutive( 3243 // Select expired items. 3244 [ 3245 'watchlist_expiry', 3246 'we_item', 3247 [ 'we_expiry <= 20200101000000' ], 3248 'WatchedItemStore::removeExpired', 3249 [ 'LIMIT' => 2 ] 3250 ], 3251 // Select orphaned items. 3252 [ 3253 [ 'watchlist_expiry', 'watchlist' ], 3254 'we_item', 3255 [ 'wl_id' => null, 'we_expiry' => null ], 3256 'WatchedItemStore::removeExpired', 3257 [], 3258 [ 'watchlist' => [ 'LEFT JOIN', 'wl_id = we_item' ] ] 3259 ] 3260 ) 3261 ->willReturnOnConsecutiveCalls( 3262 [ 1, 2 ], 3263 [ 3 ] 3264 ); 3265 3266 // Return whatever is passed to makeList, to be tested below. 3267 $mockDb->expects( $this->once() ) 3268 ->method( 'makeList' ) 3269 ->willReturnArgument( 0 ); 3270 3271 // Delete from watchlist and watchlist_expiry. 3272 $mockDb->expects( $this->exactly( 3 ) ) 3273 ->method( 'delete' ) 3274 ->withConsecutive( 3275 // Delete expired items from watchlist 3276 [ 3277 'watchlist', 3278 [ 'wl_id' => [ 1, 2 ] ], 3279 'WatchedItemStore::removeExpired' 3280 ], 3281 // Delete expired items from watchlist_expiry 3282 [ 3283 'watchlist_expiry', 3284 [ 'we_item' => [ 1, 2 ] ], 3285 'WatchedItemStore::removeExpired' 3286 ], 3287 // Delete orphaned items 3288 [ 3289 'watchlist_expiry', 3290 [ 'we_item' => [ 3 ] ], 3291 'WatchedItemStore::removeExpired' 3292 ] 3293 ); 3294 3295 $mockCache = $this->getMockCache(); 3296 $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] ); 3297 $store->removeExpired( 2, true ); 3298 } 3299} 3300