X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/d57edfddd6c01f0ed6b1a84019649cdf6cddd5f8..83d871ca0d985c6d586b323bf96161afb510ebf6:/tests/phpunit/includes/user/BotPasswordTest.php diff --git a/tests/phpunit/includes/user/BotPasswordTest.php b/tests/phpunit/includes/user/BotPasswordTest.php new file mode 100644 index 00000000..09cf3507 --- /dev/null +++ b/tests/phpunit/includes/user/BotPasswordTest.php @@ -0,0 +1,420 @@ +setMwGlobals( [ + 'wgEnableBotPasswords' => true, + 'wgBotPasswordsDatabase' => false, + 'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock', + 'wgGrantPermissions' => [ + 'test' => [ 'read' => true ], + ], + 'wgUserrightsInterwikiDelimiter' => '@', + ] ); + + $this->testUser = $this->getMutableTestUser(); + $this->testUserName = $this->testUser->getUser()->getName(); + + $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock1->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( true ) ); + $mock1->expects( $this->any() )->method( 'lookupUserNames' ) + ->will( $this->returnValue( [ $this->testUserName => 42, 'UTDummy' => 43, 'UTInvalid' => 0 ] ) ); + $mock1->expects( $this->never() )->method( 'lookupCentralIds' ); + + $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock2->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( false ) ); + $mock2->expects( $this->any() )->method( 'lookupUserNames' ) + ->will( $this->returnArgument( 0 ) ); + $mock2->expects( $this->never() )->method( 'lookupCentralIds' ); + + $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', [ + 'BotPasswordTest OkMock' => [ 'factory' => function () use ( $mock1 ) { + return $mock1; + } ], + 'BotPasswordTest FailMock' => [ 'factory' => function () use ( $mock2 ) { + return $mock2; + } ], + ] ); + + CentralIdLookup::resetCache(); + } + + public function addDBData() { + $passwordFactory = new \PasswordFactory(); + $passwordFactory->init( \RequestContext::getMain()->getConfig() ); + $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' ); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( + 'bot_passwords', + [ 'bp_user' => [ 42, 43 ], 'bp_app_id' => 'BotPassword' ], + __METHOD__ + ); + $dbw->insert( + 'bot_passwords', + [ + [ + 'bp_user' => 42, + 'bp_app_id' => 'BotPassword', + 'bp_password' => $passwordHash->toString(), + 'bp_token' => 'token!', + 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}', + 'bp_grants' => '["test"]', + ], + [ + 'bp_user' => 43, + 'bp_app_id' => 'BotPassword', + 'bp_password' => $passwordHash->toString(), + 'bp_token' => 'token!', + 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}', + 'bp_grants' => '["test"]', + ], + ], + __METHOD__ + ); + } + + public function testBasics() { + $user = $this->testUser->getUser(); + $bp = BotPassword::newFromUser( $user, 'BotPassword' ); + $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertTrue( $bp->isSaved() ); + $this->assertSame( 42, $bp->getUserCentralId() ); + $this->assertSame( 'BotPassword', $bp->getAppId() ); + $this->assertSame( 'token!', trim( $bp->getToken(), " \0" ) ); + $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() ); + $this->assertSame( [ 'test' ], $bp->getGrants() ); + + $this->assertNull( BotPassword::newFromUser( $user, 'DoesNotExist' ) ); + + $this->setMwGlobals( [ + 'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock' + ] ); + $this->assertNull( BotPassword::newFromUser( $user, 'BotPassword' ) ); + + $this->assertSame( '@', BotPassword::getSeparator() ); + $this->setMwGlobals( [ + 'wgUserrightsInterwikiDelimiter' => '#', + ] ); + $this->assertSame( '#', BotPassword::getSeparator() ); + } + + public function testUnsaved() { + $user = $this->testUser->getUser(); + $bp = BotPassword::newUnsaved( [ + 'user' => $user, + 'appId' => 'DoesNotExist' + ] ); + $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertFalse( $bp->isSaved() ); + $this->assertSame( 42, $bp->getUserCentralId() ); + $this->assertSame( 'DoesNotExist', $bp->getAppId() ); + $this->assertEquals( MWRestrictions::newDefault(), $bp->getRestrictions() ); + $this->assertSame( [], $bp->getGrants() ); + + $bp = BotPassword::newUnsaved( [ + 'username' => 'UTDummy', + 'appId' => 'DoesNotExist2', + 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ), + 'grants' => [ 'test' ], + ] ); + $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertFalse( $bp->isSaved() ); + $this->assertSame( 43, $bp->getUserCentralId() ); + $this->assertSame( 'DoesNotExist2', $bp->getAppId() ); + $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() ); + $this->assertSame( [ 'test' ], $bp->getGrants() ); + + $user = $this->testUser->getUser(); + $bp = BotPassword::newUnsaved( [ + 'centralId' => 45, + 'appId' => 'DoesNotExist' + ] ); + $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertFalse( $bp->isSaved() ); + $this->assertSame( 45, $bp->getUserCentralId() ); + $this->assertSame( 'DoesNotExist', $bp->getAppId() ); + + $user = $this->testUser->getUser(); + $bp = BotPassword::newUnsaved( [ + 'user' => $user, + 'appId' => 'BotPassword' + ] ); + $this->assertInstanceOf( 'BotPassword', $bp ); + $this->assertFalse( $bp->isSaved() ); + + $this->assertNull( BotPassword::newUnsaved( [ + 'user' => $user, + 'appId' => '', + ] ) ); + $this->assertNull( BotPassword::newUnsaved( [ + 'user' => $user, + 'appId' => str_repeat( 'X', BotPassword::APPID_MAXLENGTH + 1 ), + ] ) ); + $this->assertNull( BotPassword::newUnsaved( [ + 'user' => $this->testUserName, + 'appId' => 'Ok', + ] ) ); + $this->assertNull( BotPassword::newUnsaved( [ + 'username' => 'UTInvalid', + 'appId' => 'Ok', + ] ) ); + $this->assertNull( BotPassword::newUnsaved( [ + 'appId' => 'Ok', + ] ) ); + } + + public function testGetPassword() { + $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); + + $password = $bp->getPassword(); + $this->assertInstanceOf( 'Password', $password ); + $this->assertTrue( $password->equals( 'foobaz' ) ); + + $bp->centralId = 44; + $password = $bp->getPassword(); + $this->assertInstanceOf( 'InvalidPassword', $password ); + + $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'bot_passwords', + [ 'bp_password' => 'garbage' ], + [ 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ], + __METHOD__ + ); + $password = $bp->getPassword(); + $this->assertInstanceOf( 'InvalidPassword', $password ); + } + + public function testInvalidateAllPasswordsForUser() { + $bp1 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); + $bp2 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 43, 'BotPassword' ) ); + + $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' ); + $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' ); + BotPassword::invalidateAllPasswordsForUser( $this->testUserName ); + $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() ); + $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() ); + + $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); + $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() ); + } + + public function testRemoveAllPasswordsForUser() { + $this->assertNotNull( BotPassword::newFromCentralId( 42, 'BotPassword' ), 'sanity check' ); + $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ), 'sanity check' ); + + BotPassword::removeAllPasswordsForUser( $this->testUserName ); + + $this->assertNull( BotPassword::newFromCentralId( 42, 'BotPassword' ) ); + $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) ); + } + + /** + * @dataProvider provideCanonicalizeLoginData + */ + public function testCanonicalizeLoginData( $username, $password, $expectedResult ) { + $result = BotPassword::canonicalizeLoginData( $username, $password ); + if ( is_array( $expectedResult ) ) { + $this->assertArrayEquals( $expectedResult, $result, true, true ); + } else { + $this->assertSame( $expectedResult, $result ); + } + } + + public function provideCanonicalizeLoginData() { + return [ + [ 'user', 'pass', false ], + [ 'user', 'abc@def', false ], + [ 'legacy@user', 'pass', false ], + [ 'user@bot', '12345678901234567890123456789012', + [ 'user@bot', '12345678901234567890123456789012', true ] ], + [ 'user', 'bot@12345678901234567890123456789012', + [ 'user@bot', '12345678901234567890123456789012', true ] ], + [ 'user', 'bot@12345678901234567890123456789012345', + [ 'user@bot', '12345678901234567890123456789012345', true ] ], + [ 'user', 'bot@x@12345678901234567890123456789012', + [ 'user@bot@x', '12345678901234567890123456789012', true ] ], + ]; + } + + public function testLogin() { + // Test failure when bot passwords aren't enabled + $this->setMwGlobals( 'wgEnableBotPasswords', false ); + $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest ); + $this->assertEquals( Status::newFatal( 'botpasswords-disabled' ), $status ); + $this->setMwGlobals( 'wgEnableBotPasswords', true ); + + // Test failure when BotPasswordSessionProvider isn't configured + $manager = new SessionManager( [ + 'logger' => new Psr\Log\NullLogger, + 'store' => new EmptyBagOStuff, + ] ); + $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager ); + $this->assertNull( + $manager->getProvider( MediaWiki\Session\BotPasswordSessionProvider::class ), + 'sanity check' + ); + $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest ); + $this->assertEquals( Status::newFatal( 'botpasswords-no-provider' ), $status ); + ScopedCallback::consume( $reset ); + + // Now configure BotPasswordSessionProvider for further tests... + $mainConfig = RequestContext::getMain()->getConfig(); + $config = new HashConfig( [ + 'SessionProviders' => $mainConfig->get( 'SessionProviders' ) + [ + MediaWiki\Session\BotPasswordSessionProvider::class => [ + 'class' => MediaWiki\Session\BotPasswordSessionProvider::class, + 'args' => [ [ 'priority' => 40 ] ], + ] + ], + ] ); + $manager = new SessionManager( [ + 'config' => new MultiConfig( [ $config, RequestContext::getMain()->getConfig() ] ), + 'logger' => new Psr\Log\NullLogger, + 'store' => new EmptyBagOStuff, + ] ); + $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager ); + + // No "@"-thing in the username + $status = BotPassword::login( $this->testUserName, 'foobaz', new FauxRequest ); + $this->assertEquals( Status::newFatal( 'botpasswords-invalid-name', '@' ), $status ); + + // No base user + $status = BotPassword::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest ); + $this->assertEquals( Status::newFatal( 'nosuchuser', 'UTDummy' ), $status ); + + // No bot password + $status = BotPassword::login( "{$this->testUserName}@DoesNotExist", 'foobaz', new FauxRequest ); + $this->assertEquals( + Status::newFatal( 'botpasswords-not-exist', $this->testUserName, 'DoesNotExist' ), + $status + ); + + // Failed restriction + $request = $this->getMockBuilder( 'FauxRequest' ) + ->setMethods( [ 'getIP' ] ) + ->getMock(); + $request->expects( $this->any() )->method( 'getIP' ) + ->will( $this->returnValue( '10.0.0.1' ) ); + $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', $request ); + $this->assertEquals( Status::newFatal( 'botpasswords-restriction-failed' ), $status ); + + // Wrong password + $status = BotPassword::login( + "{$this->testUserName}@BotPassword", $this->testUser->getPassword(), new FauxRequest ); + $this->assertEquals( Status::newFatal( 'wrongpassword' ), $status ); + + // Success! + $request = new FauxRequest; + $this->assertNotInstanceOf( + MediaWiki\Session\BotPasswordSessionProvider::class, + $request->getSession()->getProvider(), + 'sanity check' + ); + $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', $request ); + $this->assertInstanceOf( 'Status', $status ); + $this->assertTrue( $status->isGood() ); + $session = $status->getValue(); + $this->assertInstanceOf( MediaWiki\Session\Session::class, $session ); + $this->assertInstanceOf( + MediaWiki\Session\BotPasswordSessionProvider::class, $session->getProvider() + ); + $this->assertSame( $session->getId(), $request->getSession()->getId() ); + + ScopedCallback::consume( $reset ); + } + + /** + * @dataProvider provideSave + * @param string|null $password + */ + public function testSave( $password ) { + $passwordFactory = new \PasswordFactory(); + $passwordFactory->init( \RequestContext::getMain()->getConfig() ); + + $bp = BotPassword::newUnsaved( [ + 'centralId' => 42, + 'appId' => 'TestSave', + 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ), + 'grants' => [ 'test' ], + ] ); + $this->assertFalse( $bp->isSaved(), 'sanity check' ); + $this->assertNull( + BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check' + ); + + $passwordHash = $password ? $passwordFactory->newFromPlaintext( $password ) : null; + $this->assertFalse( $bp->save( 'update', $passwordHash ) ); + $this->assertTrue( $bp->save( 'insert', $passwordHash ) ); + $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ); + $this->assertInstanceOf( 'BotPassword', $bp2 ); + $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() ); + $this->assertEquals( $bp->getAppId(), $bp2->getAppId() ); + $this->assertEquals( $bp->getToken(), $bp2->getToken() ); + $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() ); + $this->assertEquals( $bp->getGrants(), $bp2->getGrants() ); + $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword(); + if ( $password === null ) { + $this->assertInstanceOf( 'InvalidPassword', $pw ); + } else { + $this->assertTrue( $pw->equals( $password ) ); + } + + $token = $bp->getToken(); + $this->assertEquals( 42, $bp->getUserCentralId() ); + $this->assertEquals( 'TestSave', $bp->getAppId() ); + $this->assertFalse( $bp->save( 'insert' ) ); + $this->assertTrue( $bp->save( 'update' ) ); + $this->assertNotEquals( $token, $bp->getToken() ); + $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ); + $this->assertInstanceOf( 'BotPassword', $bp2 ); + $this->assertEquals( $bp->getToken(), $bp2->getToken() ); + $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword(); + if ( $password === null ) { + $this->assertInstanceOf( 'InvalidPassword', $pw ); + } else { + $this->assertTrue( $pw->equals( $password ) ); + } + + $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' ); + $token = $bp->getToken(); + $this->assertTrue( $bp->save( 'update', $passwordHash ) ); + $this->assertNotEquals( $token, $bp->getToken() ); + $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword(); + $this->assertTrue( $pw->equals( 'XXX' ) ); + + $this->assertTrue( $bp->delete() ); + $this->assertFalse( $bp->isSaved() ); + $this->assertNull( BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ) ); + + $this->assertFalse( $bp->save( 'foobar' ) ); + } + + public static function provideSave() { + return [ + [ null ], + [ 'foobar' ], + ]; + } +}