X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/tests/phpunit/includes/WatchedItemStoreUnitTest.php diff --git a/tests/phpunit/includes/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/WatchedItemStoreUnitTest.php new file mode 100644 index 00000000..950e2208 --- /dev/null +++ b/tests/phpunit/includes/WatchedItemStoreUnitTest.php @@ -0,0 +1,2674 @@ +createMock( IDatabase::class ); + } + + /** + * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer + */ + private function getMockLoadBalancer( + $mockDb, + $expectedConnectionType = null + ) { + $mock = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + if ( $expectedConnectionType !== null ) { + $mock->expects( $this->any() ) + ->method( 'getConnectionRef' ) + ->with( $expectedConnectionType ) + ->will( $this->returnValue( $mockDb ) ); + } else { + $mock->expects( $this->any() ) + ->method( 'getConnectionRef' ) + ->will( $this->returnValue( $mockDb ) ); + } + return $mock; + } + + /** + * @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff + */ + private function getMockCache() { + $mock = $this->getMockBuilder( HashBagOStuff::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'makeKey' ) + ->will( $this->returnCallback( function () { + return implode( ':', func_get_args() ); + } ) ); + return $mock; + } + + /** + * @return PHPUnit_Framework_MockObject_MockObject|ReadOnlyMode + */ + private function getMockReadOnlyMode( $readOnly = false ) { + $mock = $this->getMockBuilder( ReadOnlyMode::class ) + ->disableOriginalConstructor() + ->getMock(); + $mock->expects( $this->any() ) + ->method( 'isReadOnly' ) + ->will( $this->returnValue( $readOnly ) ); + return $mock; + } + + /** + * @param int $id + * @return PHPUnit_Framework_MockObject_MockObject|User + */ + private function getMockNonAnonUserWithId( $id ) { + $mock = $this->createMock( User::class ); + $mock->expects( $this->any() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( false ) ); + $mock->expects( $this->any() ) + ->method( 'getId' ) + ->will( $this->returnValue( $id ) ); + return $mock; + } + + /** + * @return User + */ + private function getAnonUser() { + return User::newFromName( 'Anon_User' ); + } + + private function getFakeRow( array $rowValues ) { + $fakeRow = new stdClass(); + foreach ( $rowValues as $valueName => $value ) { + $fakeRow->$valueName = $value; + } + return $fakeRow; + } + + private function newWatchedItemStore( LoadBalancer $loadBalancer, HashBagOStuff $cache, + ReadOnlyMode $readOnlyMode + ) { + return new WatchedItemStore( + $loadBalancer, + $cache, + $readOnlyMode + ); + } + + public function testCountWatchedItems() { + $user = $this->getMockNonAnonUserWithId( 1 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectField' ) + ->with( + 'watchlist', + 'COUNT(*)', + [ + 'wl_user' => $user->getId(), + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 12 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( 12, $store->countWatchedItems( $user ) ); + } + + public function testCountWatchers() { + $titleValue = new TitleValue( 0, 'SomeDbKey' ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectField' ) + ->with( + 'watchlist', + 'COUNT(*)', + [ + 'wl_namespace' => $titleValue->getNamespace(), + 'wl_title' => $titleValue->getDBkey(), + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 7 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( 7, $store->countWatchers( $titleValue ) ); + } + + public function testCountWatchersMultiple() { + $titleValues = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 0, 'OtherDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + + $dbResult = [ + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] + ), + ]; + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ], + [ 'makeWhereFrom2d return value' ], + $this->isType( 'string' ), + [ + 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], + ] + ) + ->will( + $this->returnValue( $dbResult ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $expected = [ + 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], + 1 => [ 'AnotherDbKey' => 500 ], + ]; + $this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) ); + } + + public function provideIntWithDbUnsafeVersion() { + return [ + [ 50 ], + [ "50; DROP TABLE watchlist;\n--" ], + ]; + } + + /** + * @dataProvider provideIntWithDbUnsafeVersion + */ + public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers ) { + $titleValues = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 0, 'OtherDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + + $dbResult = [ + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] + ), + ]; + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ], + [ 'makeWhereFrom2d return value' ], + $this->isType( 'string' ), + [ + 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], + 'HAVING' => 'COUNT(*) >= 50', + ] + ) + ->will( + $this->returnValue( $dbResult ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $expected = [ + 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], + 1 => [ 'AnotherDbKey' => 500 ], + ]; + $this->assertEquals( + $expected, + $store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] ) + ); + } + + public function testCountVisitingWatchers() { + $titleValue = new TitleValue( 0, 'SomeDbKey' ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectField' ) + ->with( + 'watchlist', + 'COUNT(*)', + [ + 'wl_namespace' => $titleValue->getNamespace(), + 'wl_title' => $titleValue->getDBkey(), + 'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL', + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 7 ) ); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'addQuotes' ) + ->will( $this->returnCallback( function ( $value ) { + return "'$value'"; + } ) ); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) ); + } + + public function testCountVisitingWatchersMultiple() { + $titleValuesWithThresholds = [ + [ new TitleValue( 0, 'SomeDbKey' ), '111' ], + [ new TitleValue( 0, 'OtherDbKey' ), '111' ], + [ new TitleValue( 1, 'AnotherDbKey' ), '123' ], + ]; + + $dbResult = [ + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ), + ]; + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 2 * 3 ) ) + ->method( 'addQuotes' ) + ->will( $this->returnCallback( function ( $value ) { + return "'$value'"; + } ) ); + $mockDb->expects( $this->exactly( 3 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + $mockDb->expects( $this->any() ) + ->method( 'makeList' ) + ->with( + $this->isType( 'array' ), + $this->isType( 'int' ) + ) + ->will( $this->returnCallback( function ( $a, $conj ) { + $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; + return join( $sqlConj, array_map( function ( $s ) { + return '(' . $s . ')'; + }, $a + ) ); + } ) ); + $mockDb->expects( $this->never() ) + ->method( 'makeWhereFrom2d' ); + + $expectedCond = + '((wl_namespace = 0) AND (' . + "(((wl_title = 'SomeDbKey') AND (" . + "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . + ')) OR (' . + "(wl_title = 'OtherDbKey') AND (" . + "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . + '))))' . + ') OR ((wl_namespace = 1) AND (' . + "(((wl_title = 'AnotherDbKey') AND (". + "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" . + ')))))'; + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], + $expectedCond, + $this->isType( 'string' ), + [ + 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], + ] + ) + ->will( + $this->returnValue( $dbResult ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $expected = [ + 0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ], + 1 => [ 'AnotherDbKey' => 500 ], + ]; + $this->assertEquals( + $expected, + $store->countVisitingWatchersMultiple( $titleValuesWithThresholds ) + ); + } + + public function testCountVisitingWatchersMultiple_withMissingTargets() { + $titleValuesWithThresholds = [ + [ new TitleValue( 0, 'SomeDbKey' ), '111' ], + [ new TitleValue( 0, 'OtherDbKey' ), '111' ], + [ new TitleValue( 1, 'AnotherDbKey' ), '123' ], + [ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ], + [ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ], + ]; + + $dbResult = [ + $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ), + $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ), + $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ), + $this->getFakeRow( + [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] + ), + $this->getFakeRow( + [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 200 ] + ), + ]; + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 2 * 3 ) ) + ->method( 'addQuotes' ) + ->will( $this->returnCallback( function ( $value ) { + return "'$value'"; + } ) ); + $mockDb->expects( $this->exactly( 3 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + $mockDb->expects( $this->any() ) + ->method( 'makeList' ) + ->with( + $this->isType( 'array' ), + $this->isType( 'int' ) + ) + ->will( $this->returnCallback( function ( $a, $conj ) { + $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR '; + return join( $sqlConj, array_map( function ( $s ) { + return '(' . $s . ')'; + }, $a + ) ); + } ) ); + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + + $expectedCond = + '((wl_namespace = 0) AND (' . + "(((wl_title = 'SomeDbKey') AND (" . + "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . + ')) OR (' . + "(wl_title = 'OtherDbKey') AND (" . + "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" . + '))))' . + ') OR ((wl_namespace = 1) AND (' . + "(((wl_title = 'AnotherDbKey') AND (". + "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" . + '))))' . + ') OR ' . + '(makeWhereFrom2d return value)'; + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], + $expectedCond, + $this->isType( 'string' ), + [ + 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], + ] + ) + ->will( + $this->returnValue( $dbResult ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $expected = [ + 0 => [ + 'SomeDbKey' => 100, 'OtherDbKey' => 300, + 'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200 + ], + 1 => [ 'AnotherDbKey' => 500 ], + ]; + $this->assertEquals( + $expected, + $store->countVisitingWatchersMultiple( $titleValuesWithThresholds ) + ); + } + + /** + * @dataProvider provideIntWithDbUnsafeVersion + */ + public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers ) { + $titleValuesWithThresholds = [ + [ new TitleValue( 0, 'SomeDbKey' ), '111' ], + [ new TitleValue( 0, 'OtherDbKey' ), '111' ], + [ new TitleValue( 1, 'AnotherDbKey' ), '123' ], + ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->any() ) + ->method( 'makeList' ) + ->will( $this->returnValue( 'makeList return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], + 'makeList return value', + $this->isType( 'string' ), + [ + 'GROUP BY' => [ 'wl_namespace', 'wl_title' ], + 'HAVING' => 'COUNT(*) >= 50', + ] + ) + ->will( + $this->returnValue( [] ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $expected = [ + 0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ], + 1 => [ 'AnotherDbKey' => 0 ], + ]; + $this->assertEquals( + $expected, + $store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers ) + ); + } + + public function testCountUnreadNotifications() { + $user = $this->getMockNonAnonUserWithId( 1 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectRowCount' ) + ->with( + 'watchlist', + '1', + [ + "wl_notificationtimestamp IS NOT NULL", + 'wl_user' => 1, + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 9 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( 9, $store->countUnreadNotifications( $user ) ); + } + + /** + * @dataProvider provideIntWithDbUnsafeVersion + */ + public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) { + $user = $this->getMockNonAnonUserWithId( 1 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectRowCount' ) + ->with( + 'watchlist', + '1', + [ + "wl_notificationtimestamp IS NOT NULL", + 'wl_user' => 1, + ], + $this->isType( 'string' ), + [ 'LIMIT' => 50 ] + ) + ->will( $this->returnValue( 50 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertSame( + true, + $store->countUnreadNotifications( $user, $limit ) + ); + } + + /** + * @dataProvider provideIntWithDbUnsafeVersion + */ + public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) { + $user = $this->getMockNonAnonUserWithId( 1 ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'selectRowCount' ) + ->with( + 'watchlist', + '1', + [ + "wl_notificationtimestamp IS NOT NULL", + 'wl_user' => 1, + ], + $this->isType( 'string' ), + [ 'LIMIT' => 50 ] + ) + ->will( $this->returnValue( 9 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + 9, + $store->countUnreadNotifications( $user, $limit ) + ); + } + + public function testDuplicateEntry_nothingToDuplicate() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => 0, + 'wl_title' => 'Old_Title', + ], + 'WatchedItemStore::duplicateEntry', + [ 'FOR UPDATE' ] + ) + ->will( $this->returnValue( new FakeResultWrapper( [] ) ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + + $store->duplicateEntry( + Title::newFromText( 'Old_Title' ), + Title::newFromText( 'New_Title' ) + ); + } + + public function testDuplicateEntry_somethingToDuplicate() { + $fakeRows = [ + $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ), + $this->getFakeRow( [ 'wl_user' => 2, 'wl_notificationtimestamp' => null ] ), + ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->at( 0 ) ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => 0, + 'wl_title' => 'Old_Title', + ] + ) + ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) ); + $mockDb->expects( $this->at( 1 ) ) + ->method( 'replace' ) + ->with( + 'watchlist', + [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], + [ + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'New_Title', + 'wl_notificationtimestamp' => '20151212010101', + ], + [ + 'wl_user' => 2, + 'wl_namespace' => 0, + 'wl_title' => 'New_Title', + 'wl_notificationtimestamp' => null, + ], + ], + $this->isType( 'string' ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $store->duplicateEntry( + Title::newFromText( 'Old_Title' ), + Title::newFromText( 'New_Title' ) + ); + } + + public function testDuplicateAllAssociatedEntries_nothingToDuplicate() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->at( 0 ) ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => 0, + 'wl_title' => 'Old_Title', + ] + ) + ->will( $this->returnValue( new FakeResultWrapper( [] ) ) ); + $mockDb->expects( $this->at( 1 ) ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => 1, + 'wl_title' => 'Old_Title', + ] + ) + ->will( $this->returnValue( new FakeResultWrapper( [] ) ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $store->duplicateAllAssociatedEntries( + Title::newFromText( 'Old_Title' ), + Title::newFromText( 'New_Title' ) + ); + } + + public function provideLinkTargetPairs() { + return [ + [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ], + [ new TitleValue( 0, 'Old_Title' ), new TitleValue( 0, 'New_Title' ) ], + ]; + } + + /** + * @dataProvider provideLinkTargetPairs + */ + public function testDuplicateAllAssociatedEntries_somethingToDuplicate( + LinkTarget $oldTarget, + LinkTarget $newTarget + ) { + $fakeRows = [ + $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ), + ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->at( 0 ) ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => $oldTarget->getNamespace(), + 'wl_title' => $oldTarget->getDBkey(), + ] + ) + ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) ); + $mockDb->expects( $this->at( 1 ) ) + ->method( 'replace' ) + ->with( + 'watchlist', + [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], + [ + [ + 'wl_user' => 1, + 'wl_namespace' => $newTarget->getNamespace(), + 'wl_title' => $newTarget->getDBkey(), + 'wl_notificationtimestamp' => '20151212010101', + ], + ], + $this->isType( 'string' ) + ); + $mockDb->expects( $this->at( 2 ) ) + ->method( 'select' ) + ->with( + 'watchlist', + [ + 'wl_user', + 'wl_notificationtimestamp', + ], + [ + 'wl_namespace' => $oldTarget->getNamespace() + 1, + 'wl_title' => $oldTarget->getDBkey(), + ] + ) + ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) ); + $mockDb->expects( $this->at( 3 ) ) + ->method( 'replace' ) + ->with( + 'watchlist', + [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], + [ + [ + 'wl_user' => 1, + 'wl_namespace' => $newTarget->getNamespace() + 1, + 'wl_title' => $newTarget->getDBkey(), + 'wl_notificationtimestamp' => '20151212010101', + ], + ], + $this->isType( 'string' ) + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $store->duplicateAllAssociatedEntries( + $oldTarget, + $newTarget + ); + } + + public function testAddWatch_nonAnonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'insert' ) + ->with( + 'watchlist', + [ + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'Some_Page', + 'wl_notificationtimestamp' => null, + ] + ] + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:Some_Page:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $store->addWatch( + $this->getMockNonAnonUserWithId( 1 ), + Title::newFromText( 'Some_Page' ) + ); + } + + public function testAddWatch_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'insert' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $store->addWatch( + $this->getAnonUser(), + Title::newFromText( 'Some_Page' ) + ); + } + + public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() { + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $this->getMockDb() ), + $this->getMockCache(), + $this->getMockReadOnlyMode( true ) + ); + + $this->assertFalse( + $store->addWatchBatchForUser( + $this->getMockNonAnonUserWithId( 1 ), + [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ] + ) + ); + } + + public function testAddWatchBatchForUser_nonAnonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'insert' ) + ->with( + 'watchlist', + [ + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'Some_Page', + 'wl_notificationtimestamp' => null, + ], + [ + 'wl_user' => 1, + 'wl_namespace' => 1, + 'wl_title' => 'Some_Page', + 'wl_notificationtimestamp' => null, + ] + ] + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->exactly( 2 ) ) + ->method( 'delete' ); + $mockCache->expects( $this->at( 1 ) ) + ->method( 'delete' ) + ->with( '0:Some_Page:1' ); + $mockCache->expects( $this->at( 3 ) ) + ->method( 'delete' ) + ->with( '1:Some_Page:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $mockUser = $this->getMockNonAnonUserWithId( 1 ); + + $this->assertTrue( + $store->addWatchBatchForUser( + $mockUser, + [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ] + ) + ); + } + + public function testAddWatchBatchForUser_anonymousUsersAreSkipped() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'insert' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->addWatchBatchForUser( + $this->getAnonUser(), + [ new TitleValue( 0, 'Other_Page' ) ] + ) + ); + } + + public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'insert' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->addWatchBatchForUser( $user, [] ) + ); + } + + public function testLoadWatchedItem_existingItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( + '0:SomeDbKey:1' + ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $watchedItem = $store->loadWatchedItem( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ); + $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + $this->assertEquals( 1, $watchedItem->getUser()->getId() ); + $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() ); + $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() ); + } + + public function testLoadWatchedItem_noItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( [] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->loadWatchedItem( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testLoadWatchedItem_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->loadWatchedItem( + $this->getAnonUser(), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testRemoveWatch_existingItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'delete' ) + ->with( + 'watchlist', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ); + $mockDb->expects( $this->once() ) + ->method( 'affectedRows' ) + ->will( $this->returnValue( 1 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->removeWatch( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testRemoveWatch_noItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'delete' ) + ->with( + 'watchlist', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ); + $mockDb->expects( $this->once() ) + ->method( 'affectedRows' ) + ->will( $this->returnValue( 0 ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->removeWatch( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testRemoveWatch_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->removeWatch( + $this->getAnonUser(), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testGetWatchedItem_existingItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( + '0:SomeDbKey:1' + ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( + '0:SomeDbKey:1' + ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $watchedItem = $store->getWatchedItem( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ); + $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + $this->assertEquals( 1, $watchedItem->getUser()->getId() ); + $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() ); + $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() ); + } + + public function testGetWatchedItem_cachedItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockUser = $this->getMockNonAnonUserWithId( 1 ); + $linkTarget = new TitleValue( 0, 'SomeDbKey' ); + $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( + '0:SomeDbKey:1' + ) + ->will( $this->returnValue( $cachedItem ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + $cachedItem, + $store->getWatchedItem( + $mockUser, + $linkTarget + ) + ); + } + + public function testGetWatchedItem_noItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( [] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( false ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->getWatchedItem( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testGetWatchedItem_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->getWatchedItem( + $this->getAnonUser(), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testGetWatchedItemsForUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ 'wl_user' => 1 ] + ) + ->will( $this->returnValue( [ + $this->getFakeRow( [ + 'wl_namespace' => 0, + 'wl_title' => 'Foo1', + 'wl_notificationtimestamp' => '20151212010101', + ] ), + $this->getFakeRow( [ + 'wl_namespace' => 1, + 'wl_title' => 'Foo2', + 'wl_notificationtimestamp' => null, + ] ), + ] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + $user = $this->getMockNonAnonUserWithId( 1 ); + + $watchedItems = $store->getWatchedItemsForUser( $user ); + + $this->assertInternalType( 'array', $watchedItems ); + $this->assertCount( 2, $watchedItems ); + foreach ( $watchedItems as $watchedItem ) { + $this->assertInstanceOf( 'WatchedItem', $watchedItem ); + } + $this->assertEquals( + new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ), + $watchedItems[0] + ); + $this->assertEquals( + new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ), + $watchedItems[1] + ); + } + + public function provideDbTypes() { + return [ + [ false, DB_REPLICA ], + [ true, DB_MASTER ], + ]; + } + + /** + * @dataProvider provideDbTypes + */ + public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) { + $mockDb = $this->getMockDb(); + $mockCache = $this->getMockCache(); + $mockLoadBalancer = $this->getMockLoadBalancer( $mockDb, $dbType ); + $user = $this->getMockNonAnonUserWithId( 1 ); + + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ 'wl_user' => 1 ], + $this->isType( 'string' ), + [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ] + ) + ->will( $this->returnValue( [] ) ); + + $store = $this->newWatchedItemStore( + $mockLoadBalancer, + $mockCache, + $this->getMockReadOnlyMode() + ); + + $watchedItems = $store->getWatchedItemsForUser( + $user, + [ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ] + ); + $this->assertEquals( [], $watchedItems ); + } + + public function testGetWatchedItemsForUser_badSortOptionThrowsException() { + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $this->getMockDb() ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + + $this->setExpectedException( 'InvalidArgumentException' ); + $store->getWatchedItemsForUser( + $this->getMockNonAnonUserWithId( 1 ), + [ 'sort' => 'foo' ] + ); + } + + public function testIsWatchedItem_existingItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( false ) ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( + '0:SomeDbKey:1' + ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->isWatched( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testIsWatchedItem_noItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( [] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( false ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->isWatched( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testIsWatchedItem_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->isWatched( + $this->getAnonUser(), + new TitleValue( 0, 'SomeDbKey' ) + ) + ); + } + + public function testGetNotificationTimestampsBatch() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + $dbResult = [ + $this->getFakeRow( [ + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + 'wl_notificationtimestamp' => '20151212010101', + ] ), + $this->getFakeRow( + [ + 'wl_namespace' => 1, + 'wl_title' => 'AnotherDbKey', + 'wl_notificationtimestamp' => null, + ] + ), + ]; + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( $dbResult ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->exactly( 2 ) ) + ->method( 'get' ) + ->withConsecutive( + [ '0:SomeDbKey:1' ], + [ '1:AnotherDbKey:1' ] + ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_notWatchedTarget() { + $targets = [ + new TitleValue( 0, 'OtherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'OtherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( $this->getFakeRow( [] ) ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:OtherDbKey:1' ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ + 0 => [ 'OtherDbKey' => false, ], + ], + $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_cachedItem() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $user = $this->getMockNonAnonUserWithId( 1 ); + $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' ); + + $mockDb = $this->getMockDb(); + + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ 1 => [ 'AnotherDbKey' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + 'watchlist', + [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], + [ + 'makeWhereFrom2d return value', + 'wl_user' => 1 + ], + $this->isType( 'string' ) + ) + ->will( $this->returnValue( [ + $this->getFakeRow( + [ 'wl_namespace' => 1, 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ] + ) + ] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->at( 1 ) ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( $cachedItem ) ); + $mockCache->expects( $this->at( 3 ) ) + ->method( 'get' ) + ->with( '1:AnotherDbKey:1' ) + ->will( $this->returnValue( null ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $user, $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_allItemsCached() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $user = $this->getMockNonAnonUserWithId( 1 ); + $cachedItems = [ + new WatchedItem( $user, $targets[0], '20151212010101' ), + new WatchedItem( $user, $targets[1], null ), + ]; + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() )->method( $this->anything() ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->at( 1 ) ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ) + ->will( $this->returnValue( $cachedItems[0] ) ); + $mockCache->expects( $this->at( 3 ) ) + ->method( 'get' ) + ->with( '1:AnotherDbKey:1' ) + ->will( $this->returnValue( $cachedItems[1] ) ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => '20151212010101', ], + 1 => [ 'AnotherDbKey' => null, ], + ], + $store->getNotificationTimestampsBatch( $user, $targets ) + ); + } + + public function testGetNotificationTimestampsBatch_anonymousUser() { + $targets = [ + new TitleValue( 0, 'SomeDbKey' ), + new TitleValue( 1, 'AnotherDbKey' ), + ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() )->method( $this->anything() ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( $this->anything() ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ + 0 => [ 'SomeDbKey' => false, ], + 1 => [ 'AnotherDbKey' => false, ], + ], + $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets ) + ); + } + + public function testResetNotificationTimestamp_anonymousUser() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->resetNotificationTimestamp( + $this->getAnonUser(), + Title::newFromText( 'SomeDbKey' ) + ) + ); + } + + public function testResetNotificationTimestamp_noItem() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( [] ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertFalse( + $store->resetNotificationTimestamp( + $this->getMockNonAnonUserWithId( 1 ), + Title::newFromText( 'SomeDbKey' ) + ) + ); + } + + public function testResetNotificationTimestamp_item() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $title = Title::newFromText( 'SomeDbKey' ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( + '0:SomeDbKey:1', + $this->isInstanceOf( WatchedItem::class ) + ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + // Note: This does not actually assert the job is correct + $callableCallCounter = 0; + $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { + $callableCallCounter++; + $this->assertInternalType( 'callable', $callable ); + }; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title + ) + ); + $this->assertEquals( 1, $callableCallCounter ); + + ScopedCallback::consume( $scopedOverride ); + } + + public function testResetNotificationTimestamp_noItemForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $title = Title::newFromText( 'SomeDbKey' ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + // Note: This does not actually assert the job is correct + $callableCallCounter = 0; + $mockCallback = function ( $callable ) use ( &$callableCallCounter ) { + $callableCallCounter++; + $this->assertInternalType( 'callable', $callable ); + }; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + 'force' + ) + ); + $this->assertEquals( 1, $callableCallCounter ); + + ScopedCallback::consume( $scopedOverride ); + } + + /** + * @param string $text + * @param int $ns + * + * @return PHPUnit_Framework_MockObject_MockObject|Title + */ + private function getMockTitle( $text, $ns = 0 ) { + $title = $this->createMock( Title::class ); + $title->expects( $this->any() ) + ->method( 'getText' ) + ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) ); + $title->expects( $this->any() ) + ->method( 'getDbKey' ) + ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) ); + $title->expects( $this->any() ) + ->method( 'getNamespace' ) + ->will( $this->returnValue( $ns ) ); + return $title; + } + + private function verifyCallbackJob( + $callback, + LinkTarget $expectedTitle, + $expectedUserId, + callable $notificationTimestampCondition + ) { + $this->assertInternalType( 'callable', $callback ); + + $callbackReflector = new ReflectionFunction( $callback ); + $vars = $callbackReflector->getStaticVariables(); + $this->assertArrayHasKey( 'job', $vars ); + $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] ); + + /** @var ActivityUpdateJob $job */ + $job = $vars['job']; + $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() ); + $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() ); + + $jobParams = $job->getParams(); + $this->assertArrayHasKey( 'type', $jobParams ); + $this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] ); + $this->assertArrayHasKey( 'userid', $jobParams ); + $this->assertEquals( $expectedUserId, $jobParams['userid'] ); + $this->assertArrayHasKey( 'notifTime', $jobParams ); + $this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) ); + } + + public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $oldid = 22; + $title = $this->getMockTitle( 'SomeTitle' ); + $title->expects( $this->once() ) + ->method( 'getNextRevisionID' ) + ->with( $oldid ) + ->will( $this->returnValue( false ) ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->never() ) + ->method( 'selectRow' ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $callableCallCounter = 0; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( + function ( $callable ) use ( &$callableCallCounter, $title, $user ) { + $callableCallCounter++; + $this->verifyCallbackJob( + $callable, + $title, + $user->getId(), + function ( $time ) { + return $time === null; + } + ); + } + ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + 'force', + $oldid + ) + ); + $this->assertEquals( 1, $callableCallCounter ); + + ScopedCallback::consume( $scopedOverride ); + } + + public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $oldid = 22; + $title = $this->getMockTitle( 'SomeDbKey' ); + $title->expects( $this->once() ) + ->method( 'getNextRevisionID' ) + ->with( $oldid ) + ->will( $this->returnValue( 33 ) ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $addUpdateCallCounter = 0; + $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( + function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { + $addUpdateCallCounter++; + $this->verifyCallbackJob( + $callable, + $title, + $user->getId(), + function ( $time ) { + return $time !== null && $time > '20151212010101'; + } + ); + } + ); + + $getTimestampCallCounter = 0; + $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( + function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { + $getTimestampCallCounter++; + $this->assertEquals( $title, $titleParam ); + $this->assertEquals( $oldid, $oldidParam ); + } + ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + 'force', + $oldid + ) + ); + $this->assertEquals( 1, $addUpdateCallCounter ); + $this->assertEquals( 1, $getTimestampCallCounter ); + + ScopedCallback::consume( $scopedOverrideDeferred ); + ScopedCallback::consume( $scopedOverrideRevision ); + } + + public function testResetNotificationTimestamp_notWatchedPageForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $oldid = 22; + $title = $this->getMockTitle( 'SomeDbKey' ); + $title->expects( $this->once() ) + ->method( 'getNextRevisionID' ) + ->with( $oldid ) + ->will( $this->returnValue( 33 ) ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( false ) ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $callableCallCounter = 0; + $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( + function ( $callable ) use ( &$callableCallCounter, $title, $user ) { + $callableCallCounter++; + $this->verifyCallbackJob( + $callable, + $title, + $user->getId(), + function ( $time ) { + return $time === null; + } + ); + } + ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + 'force', + $oldid + ) + ); + $this->assertEquals( 1, $callableCallCounter ); + + ScopedCallback::consume( $scopedOverride ); + } + + public function testResetNotificationTimestamp_futureNotificationTimestampForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $oldid = 22; + $title = $this->getMockTitle( 'SomeDbKey' ); + $title->expects( $this->once() ) + ->method( 'getNextRevisionID' ) + ->with( $oldid ) + ->will( $this->returnValue( 33 ) ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $addUpdateCallCounter = 0; + $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( + function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { + $addUpdateCallCounter++; + $this->verifyCallbackJob( + $callable, + $title, + $user->getId(), + function ( $time ) { + return $time === '30151212010101'; + } + ); + } + ); + + $getTimestampCallCounter = 0; + $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( + function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { + $getTimestampCallCounter++; + $this->assertEquals( $title, $titleParam ); + $this->assertEquals( $oldid, $oldidParam ); + } + ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + 'force', + $oldid + ) + ); + $this->assertEquals( 1, $addUpdateCallCounter ); + $this->assertEquals( 1, $getTimestampCallCounter ); + + ScopedCallback::consume( $scopedOverrideDeferred ); + ScopedCallback::consume( $scopedOverrideRevision ); + } + + public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $oldid = 22; + $title = $this->getMockTitle( 'SomeDbKey' ); + $title->expects( $this->once() ) + ->method( 'getNextRevisionID' ) + ->with( $oldid ) + ->will( $this->returnValue( 33 ) ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->with( + 'watchlist', + 'wl_notificationtimestamp', + [ + 'wl_user' => 1, + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] ) + ) ); + + $mockCache = $this->getMockCache(); + $mockDb->expects( $this->never() ) + ->method( 'get' ); + $mockDb->expects( $this->never() ) + ->method( 'set' ); + $mockDb->expects( $this->never() ) + ->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $addUpdateCallCounter = 0; + $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback( + function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) { + $addUpdateCallCounter++; + $this->verifyCallbackJob( + $callable, + $title, + $user->getId(), + function ( $time ) { + return $time === false; + } + ); + } + ); + + $getTimestampCallCounter = 0; + $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback( + function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) { + $getTimestampCallCounter++; + $this->assertEquals( $title, $titleParam ); + $this->assertEquals( $oldid, $oldidParam ); + } + ); + + $this->assertTrue( + $store->resetNotificationTimestamp( + $user, + $title, + '', + $oldid + ) + ); + $this->assertEquals( 1, $addUpdateCallCounter ); + $this->assertEquals( 1, $getTimestampCallCounter ); + + ScopedCallback::consume( $scopedOverrideDeferred ); + ScopedCallback::consume( $scopedOverrideRevision ); + } + + public function testSetNotificationTimestampsForUser_anonUser() { + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $this->getMockDb() ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) ); + } + + public function testSetNotificationTimestampsForUser_allRows() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $timestamp = '20100101010101'; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'update' ) + ->with( + 'watchlist', + [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ], + [ 'wl_user' => 1 ] + ) + ->will( $this->returnValue( true ) ); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->setNotificationTimestampsForUser( $user, $timestamp ) + ); + } + + public function testSetNotificationTimestampsForUser_nullTimestamp() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $timestamp = null; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'update' ) + ->with( + 'watchlist', + [ 'wl_notificationtimestamp' => null ], + [ 'wl_user' => 1 ] + ) + ->will( $this->returnValue( true ) ); + $mockDb->expects( $this->exactly( 0 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->setNotificationTimestampsForUser( $user, $timestamp ) + ); + } + + public function testSetNotificationTimestampsForUser_specificTargets() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $timestamp = '20100101010101'; + $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ]; + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'update' ) + ->with( + 'watchlist', + [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ], + [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ] + ) + ->will( $this->returnValue( true ) ); + $mockDb->expects( $this->exactly( 1 ) ) + ->method( 'timestamp' ) + ->will( $this->returnCallback( function ( $value ) { + return 'TS' . $value . 'TS'; + } ) ); + $mockDb->expects( $this->once() ) + ->method( 'makeWhereFrom2d' ) + ->with( + [ [ 'Foo' => 1, 'Bar' => 1 ] ], + $this->isType( 'string' ), + $this->isType( 'string' ) + ) + ->will( $this->returnValue( 'makeWhereFrom2d return value' ) ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $this->getMockCache(), + $this->getMockReadOnlyMode() + ); + + $this->assertTrue( + $store->setNotificationTimestampsForUser( $user, $timestamp, $targets ) + ); + } + + public function testUpdateNotificationTimestamp_watchersExist() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectFieldValues' ) + ->with( + 'watchlist', + 'wl_user', + [ + 'wl_user != 1', + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + 'wl_notificationtimestamp IS NULL' + ] + ) + ->will( $this->returnValue( [ '2', '3' ] ) ); + $mockDb->expects( $this->once() ) + ->method( 'update' ) + ->with( + 'watchlist', + [ 'wl_notificationtimestamp' => null ], + [ + 'wl_user' => [ 2, 3 ], + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + ] + ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $this->assertEquals( + [ 2, 3 ], + $store->updateNotificationTimestamp( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ), + '20151212010101' + ) + ); + } + + public function testUpdateNotificationTimestamp_noWatchers() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectFieldValues' ) + ->with( + 'watchlist', + 'wl_user', + [ + 'wl_user != 1', + 'wl_namespace' => 0, + 'wl_title' => 'SomeDbKey', + 'wl_notificationtimestamp IS NULL' + ] + ) + ->will( + $this->returnValue( [] ) + ); + $mockDb->expects( $this->never() ) + ->method( 'update' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->never() )->method( 'set' ); + $mockCache->expects( $this->never() )->method( 'get' ); + $mockCache->expects( $this->never() )->method( 'delete' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + $watchers = $store->updateNotificationTimestamp( + $this->getMockNonAnonUserWithId( 1 ), + new TitleValue( 0, 'SomeDbKey' ), + '20151212010101' + ); + $this->assertInternalType( 'array', $watchers ); + $this->assertEmpty( $watchers ); + } + + public function testUpdateNotificationTimestamp_clearsCachedItems() { + $user = $this->getMockNonAnonUserWithId( 1 ); + $titleValue = new TitleValue( 0, 'SomeDbKey' ); + + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'selectRow' ) + ->will( $this->returnValue( + $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] ) + ) ); + $mockDb->expects( $this->once() ) + ->method( 'selectFieldValues' ) + ->will( + $this->returnValue( [ '2', '3' ] ) + ); + $mockDb->expects( $this->once() ) + ->method( 'update' ); + + $mockCache = $this->getMockCache(); + $mockCache->expects( $this->once() ) + ->method( 'set' ) + ->with( '0:SomeDbKey:1', $this->isType( 'object' ) ); + $mockCache->expects( $this->once() ) + ->method( 'get' ) + ->with( '0:SomeDbKey:1' ); + $mockCache->expects( $this->once() ) + ->method( 'delete' ) + ->with( '0:SomeDbKey:1' ); + + $store = $this->newWatchedItemStore( + $this->getMockLoadBalancer( $mockDb ), + $mockCache, + $this->getMockReadOnlyMode() + ); + + // This will add the item to the cache + $store->getWatchedItem( $user, $titleValue ); + + $store->updateNotificationTimestamp( + $this->getMockNonAnonUserWithId( 1 ), + $titleValue, + '20151212010101' + ); + } + +}