diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/session')
15 files changed, 5759 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php b/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php new file mode 100644 index 00000000..47679940 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/BotPasswordSessionProviderTest.php @@ -0,0 +1,340 @@ +<?php + +namespace MediaWiki\Session; + +use Psr\Log\LogLevel; +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\BotPasswordSessionProvider + */ +class BotPasswordSessionProviderTest extends MediaWikiTestCase { + + private $config; + + private function getProvider( $name = null, $prefix = null ) { + global $wgSessionProviders; + + $params = [ + 'priority' => 40, + 'sessionCookieName' => $name, + 'sessionCookieOptions' => [], + ]; + if ( $prefix !== null ) { + $params['sessionCookieOptions']['prefix'] = $prefix; + } + + if ( !$this->config ) { + $this->config = new \HashConfig( [ + 'CookiePrefix' => 'wgCookiePrefix', + 'EnableBotPasswords' => true, + 'BotPasswordsDatabase' => false, + 'SessionProviders' => $wgSessionProviders + [ + BotPasswordSessionProvider::class => [ + 'class' => BotPasswordSessionProvider::class, + 'args' => [ $params ], + ] + ], + ] ); + } + $manager = new SessionManager( [ + 'config' => new \MultiConfig( [ $this->config, \RequestContext::getMain()->getConfig() ] ), + 'logger' => new \Psr\Log\NullLogger, + 'store' => new TestBagOStuff, + ] ); + + return $manager->getProvider( BotPasswordSessionProvider::class ); + } + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgEnableBotPasswords' => true, + 'wgBotPasswordsDatabase' => false, + 'wgCentralIdLookupProvider' => 'local', + 'wgGrantPermissions' => [ + 'test' => [ 'read' => true ], + ], + ] ); + } + + public function addDBDataOnce() { + $passwordFactory = new \PasswordFactory(); + $passwordFactory->init( \RequestContext::getMain()->getConfig() ); + $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' ); + + $sysop = static::getTestSysop()->getUser(); + $userId = \CentralIdLookup::factory( 'local' )->centralIdFromName( $sysop->getName() ); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( + 'bot_passwords', + [ 'bp_user' => $userId, 'bp_app_id' => 'BotPasswordSessionProvider' ], + __METHOD__ + ); + $dbw->insert( + 'bot_passwords', + [ + 'bp_user' => $userId, + 'bp_app_id' => 'BotPasswordSessionProvider', + 'bp_password' => $passwordHash->toString(), + 'bp_token' => 'token!', + 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}', + 'bp_grants' => '["test"]', + ], + __METHOD__ + ); + } + + public function testConstructor() { + try { + $provider = new BotPasswordSessionProvider(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: priority must be specified', + $ex->getMessage() + ); + } + + try { + $provider = new BotPasswordSessionProvider( [ + 'priority' => SessionInfo::MIN_PRIORITY - 1 + ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: Invalid priority', + $ex->getMessage() + ); + } + + try { + $provider = new BotPasswordSessionProvider( [ + 'priority' => SessionInfo::MAX_PRIORITY + 1 + ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\BotPasswordSessionProvider::__construct: Invalid priority', + $ex->getMessage() + ); + } + + $provider = new BotPasswordSessionProvider( [ + 'priority' => 40 + ] ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $this->assertSame( 40, $priv->priority ); + $this->assertSame( '_BPsession', $priv->sessionCookieName ); + $this->assertSame( [], $priv->sessionCookieOptions ); + + $provider = new BotPasswordSessionProvider( [ + 'priority' => 40, + 'sessionCookieName' => null, + ] ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $this->assertSame( '_BPsession', $priv->sessionCookieName ); + + $provider = new BotPasswordSessionProvider( [ + 'priority' => 40, + 'sessionCookieName' => 'Foo', + 'sessionCookieOptions' => [ 'Bar' ], + ] ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $this->assertSame( 'Foo', $priv->sessionCookieName ); + $this->assertSame( [ 'Bar' ], $priv->sessionCookieOptions ); + } + + public function testBasics() { + $provider = $this->getProvider(); + + $this->assertTrue( $provider->persistsSessionId() ); + $this->assertFalse( $provider->canChangeUser() ); + + $this->assertNull( $provider->newSessionInfo() ); + $this->assertNull( $provider->newSessionInfo( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ) ); + } + + public function testProvideSessionInfo() { + $provider = $this->getProvider(); + $request = new \FauxRequest; + $request->setCookie( '_BPsession', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'wgCookiePrefix' ); + + if ( !defined( 'MW_API' ) ) { + $this->assertNull( $provider->provideSessionInfo( $request ) ); + define( 'MW_API', 1 ); + } + + $info = $provider->provideSessionInfo( $request ); + $this->assertInstanceOf( SessionInfo::class, $info ); + $this->assertSame( 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $info->getId() ); + + $this->config->set( 'EnableBotPasswords', false ); + $this->assertNull( $provider->provideSessionInfo( $request ) ); + $this->config->set( 'EnableBotPasswords', true ); + + $this->assertNull( $provider->provideSessionInfo( new \FauxRequest ) ); + } + + public function testNewSessionInfoForRequest() { + $provider = $this->getProvider(); + $user = static::getTestSysop()->getUser(); + $request = $this->getMockBuilder( \FauxRequest::class ) + ->setMethods( [ 'getIP' ] )->getMock(); + $request->expects( $this->any() )->method( 'getIP' ) + ->will( $this->returnValue( '127.0.0.1' ) ); + $bp = \BotPassword::newFromUser( $user, 'BotPasswordSessionProvider' ); + + $session = $provider->newSessionForRequest( $user, $bp, $request ); + $this->assertInstanceOf( Session::class, $session ); + + $this->assertEquals( $session->getId(), $request->getSession()->getId() ); + $this->assertEquals( $user->getName(), $session->getUser()->getName() ); + + $this->assertEquals( [ + 'centralId' => $bp->getUserCentralId(), + 'appId' => $bp->getAppId(), + 'token' => $bp->getToken(), + 'rights' => [ 'read' ], + ], $session->getProviderMetadata() ); + + $this->assertEquals( [ 'read' ], $session->getAllowedUserRights() ); + } + + public function testCheckSessionInfo() { + $logger = new \TestLogger( true ); + $provider = $this->getProvider(); + $provider->setLogger( $logger ); + + $user = static::getTestSysop()->getUser(); + $request = $this->getMockBuilder( \FauxRequest::class ) + ->setMethods( [ 'getIP' ] )->getMock(); + $request->expects( $this->any() )->method( 'getIP' ) + ->will( $this->returnValue( '127.0.0.1' ) ); + $bp = \BotPassword::newFromUser( $user, 'BotPasswordSessionProvider' ); + + $data = [ + 'provider' => $provider, + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'userInfo' => UserInfo::newFromUser( $user, true ), + 'persisted' => false, + 'metadata' => [ + 'centralId' => $bp->getUserCentralId(), + 'appId' => $bp->getAppId(), + 'token' => $bp->getToken(), + ], + ]; + $dataMD = $data['metadata']; + + foreach ( array_keys( $data['metadata'] ) as $key ) { + $data['metadata'] = $dataMD; + unset( $data['metadata'][$key] ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data ); + $metadata = $info->getProviderMetadata(); + + $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) ); + $this->assertSame( [ + [ LogLevel::INFO, 'Session "{session}": Missing metadata: {missing}' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + } + + $data['metadata'] = $dataMD; + $data['metadata']['appId'] = 'Foobar'; + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data ); + $metadata = $info->getProviderMetadata(); + $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) ); + $this->assertSame( [ + [ LogLevel::INFO, 'Session "{session}": No BotPassword for {centralId} {appId}' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $data['metadata'] = $dataMD; + $data['metadata']['token'] = 'Foobar'; + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data ); + $metadata = $info->getProviderMetadata(); + $this->assertFalse( $provider->refreshSessionInfo( $info, $request, $metadata ) ); + $this->assertSame( [ + [ LogLevel::INFO, 'Session "{session}": BotPassword token check failed' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $request2 = $this->getMockBuilder( \FauxRequest::class ) + ->setMethods( [ 'getIP' ] )->getMock(); + $request2->expects( $this->any() )->method( 'getIP' ) + ->will( $this->returnValue( '10.0.0.1' ) ); + $data['metadata'] = $dataMD; + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data ); + $metadata = $info->getProviderMetadata(); + $this->assertFalse( $provider->refreshSessionInfo( $info, $request2, $metadata ) ); + $this->assertSame( [ + [ LogLevel::INFO, 'Session "{session}": Restrictions check failed' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, $data ); + $metadata = $info->getProviderMetadata(); + $this->assertTrue( $provider->refreshSessionInfo( $info, $request, $metadata ) ); + $this->assertSame( [], $logger->getBuffer() ); + $this->assertEquals( $dataMD + [ 'rights' => [ 'read' ] ], $metadata ); + } + + public function testGetAllowedUserRights() { + $logger = new \TestLogger( true ); + $provider = $this->getProvider(); + $provider->setLogger( $logger ); + + $backend = TestUtils::getDummySessionBackend(); + $backendPriv = TestingAccessWrapper::newFromObject( $backend ); + + try { + $provider->getAllowedUserRights( $backend ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Backend\'s provider isn\'t $this', $ex->getMessage() ); + } + + $backendPriv->provider = $provider; + $backendPriv->providerMetadata = [ 'rights' => [ 'foo', 'bar', 'baz' ] ]; + $this->assertSame( [ 'foo', 'bar', 'baz' ], $provider->getAllowedUserRights( $backend ) ); + $this->assertSame( [], $logger->getBuffer() ); + + $backendPriv->providerMetadata = [ 'foo' => 'bar' ]; + $this->assertSame( [], $provider->getAllowedUserRights( $backend ) ); + $this->assertSame( [ + [ + LogLevel::DEBUG, + 'MediaWiki\\Session\\BotPasswordSessionProvider::getAllowedUserRights: ' . + 'No provider metadata, returning no rights allowed' + ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $backendPriv->providerMetadata = [ 'rights' => 'bar' ]; + $this->assertSame( [], $provider->getAllowedUserRights( $backend ) ); + $this->assertSame( [ + [ + LogLevel::DEBUG, + 'MediaWiki\\Session\\BotPasswordSessionProvider::getAllowedUserRights: ' . + 'No provider metadata, returning no rights allowed' + ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $backendPriv->providerMetadata = null; + $this->assertSame( [], $provider->getAllowedUserRights( $backend ) ); + $this->assertSame( [ + [ + LogLevel::DEBUG, + 'MediaWiki\\Session\\BotPasswordSessionProvider::getAllowedUserRights: ' . + 'No provider metadata, returning no rights allowed' + ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + } +} diff --git a/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php b/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php new file mode 100644 index 00000000..c1df365a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/CookieSessionProviderTest.php @@ -0,0 +1,842 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use User; +use Psr\Log\LogLevel; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\CookieSessionProvider + */ +class CookieSessionProviderTest extends MediaWikiTestCase { + + private function getConfig() { + return new \HashConfig( [ + 'CookiePrefix' => 'CookiePrefix', + 'CookiePath' => 'CookiePath', + 'CookieDomain' => 'CookieDomain', + 'CookieSecure' => true, + 'CookieHttpOnly' => true, + 'SessionName' => false, + 'CookieExpiration' => 100, + 'ExtendedLoginCookieExpiration' => 200, + ] ); + } + + public function testConstructor() { + try { + new CookieSessionProvider(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\CookieSessionProvider::__construct: priority must be specified', + $ex->getMessage() + ); + } + + try { + new CookieSessionProvider( [ 'priority' => 'foo' ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority', + $ex->getMessage() + ); + } + try { + new CookieSessionProvider( [ 'priority' => SessionInfo::MIN_PRIORITY - 1 ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority', + $ex->getMessage() + ); + } + try { + new CookieSessionProvider( [ 'priority' => SessionInfo::MAX_PRIORITY + 1 ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority', + $ex->getMessage() + ); + } + + try { + new CookieSessionProvider( [ 'priority' => 1, 'cookieOptions' => null ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\CookieSessionProvider::__construct: cookieOptions must be an array', + $ex->getMessage() + ); + } + + $config = $this->getConfig(); + $p = TestingAccessWrapper::newFromObject( + new CookieSessionProvider( [ 'priority' => 1 ] ) + ); + $p->setLogger( new \TestLogger() ); + $p->setConfig( $config ); + $this->assertEquals( 1, $p->priority ); + $this->assertEquals( [ + 'callUserSetCookiesHook' => false, + 'sessionName' => 'CookiePrefix_session', + ], $p->params ); + $this->assertEquals( [ + 'prefix' => 'CookiePrefix', + 'path' => 'CookiePath', + 'domain' => 'CookieDomain', + 'secure' => true, + 'httpOnly' => true, + ], $p->cookieOptions ); + + $config->set( 'SessionName', 'SessionName' ); + $p = TestingAccessWrapper::newFromObject( + new CookieSessionProvider( [ 'priority' => 3 ] ) + ); + $p->setLogger( new \TestLogger() ); + $p->setConfig( $config ); + $this->assertEquals( 3, $p->priority ); + $this->assertEquals( [ + 'callUserSetCookiesHook' => false, + 'sessionName' => 'SessionName', + ], $p->params ); + $this->assertEquals( [ + 'prefix' => 'CookiePrefix', + 'path' => 'CookiePath', + 'domain' => 'CookieDomain', + 'secure' => true, + 'httpOnly' => true, + ], $p->cookieOptions ); + + $p = TestingAccessWrapper::newFromObject( new CookieSessionProvider( [ + 'priority' => 10, + 'callUserSetCookiesHook' => true, + 'cookieOptions' => [ + 'prefix' => 'XPrefix', + 'path' => 'XPath', + 'domain' => 'XDomain', + 'secure' => 'XSecure', + 'httpOnly' => 'XHttpOnly', + ], + 'sessionName' => 'XSession', + ] ) ); + $p->setLogger( new \TestLogger() ); + $p->setConfig( $config ); + $this->assertEquals( 10, $p->priority ); + $this->assertEquals( [ + 'callUserSetCookiesHook' => true, + 'sessionName' => 'XSession', + ], $p->params ); + $this->assertEquals( [ + 'prefix' => 'XPrefix', + 'path' => 'XPath', + 'domain' => 'XDomain', + 'secure' => 'XSecure', + 'httpOnly' => 'XHttpOnly', + ], $p->cookieOptions ); + } + + public function testBasics() { + $provider = new CookieSessionProvider( [ 'priority' => 10 ] ); + + $this->assertTrue( $provider->persistsSessionId() ); + $this->assertTrue( $provider->canChangeUser() ); + + $extendedCookies = [ 'UserID', 'UserName', 'Token' ]; + + $this->assertEquals( + $extendedCookies, + TestingAccessWrapper::newFromObject( $provider )->getExtendedLoginCookies(), + 'List of extended cookies (subclasses can add values, but we\'re calling the core one here)' + ); + + $msg = $provider->whyNoSession(); + $this->assertInstanceOf( \Message::class, $msg ); + $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() ); + } + + public function testProvideSessionInfo() { + $params = [ + 'priority' => 20, + 'sessionName' => 'session', + 'cookieOptions' => [ 'prefix' => 'x' ], + ]; + $provider = new CookieSessionProvider( $params ); + $logger = new \TestLogger( true ); + $provider->setLogger( $logger ); + $provider->setConfig( $this->getConfig() ); + $provider->setManager( new SessionManager() ); + + $user = static::getTestSysop()->getUser(); + $id = $user->getId(); + $name = $user->getName(); + $token = $user->getToken( true ); + + $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + + // No data + $request = new \FauxRequest(); + $info = $provider->provideSessionInfo( $request ); + $this->assertNull( $info ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Session key only + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertSame( 0, $info->getUserInfo()->getId() ); + $this->assertNull( $info->getUserInfo()->getName() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [ + [ + LogLevel::DEBUG, + 'Session "{session}" requested without UserID cookie', + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User, no session key + $request = new \FauxRequest(); + $request->setCookies( [ + 'xUserID' => $id, + 'xToken' => $token, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertNotSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertSame( $id, $info->getUserInfo()->getId() ); + $this->assertSame( $name, $info->getUserInfo()->getName() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User and session key + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + 'xToken' => $token, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertSame( $id, $info->getUserInfo()->getId() ); + $this->assertSame( $name, $info->getUserInfo()->getName() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User with bad token + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + 'xToken' => 'BADTOKEN', + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNull( $info ); + $this->assertSame( [ + [ + LogLevel::WARNING, + 'Session "{session}" requested with invalid Token cookie.' + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User id with no token + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertFalse( $info->getUserInfo()->isVerified() ); + $this->assertSame( $id, $info->getUserInfo()->getId() ); + $this->assertSame( $name, $info->getUserInfo()->getName() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + $request = new \FauxRequest(); + $request->setCookies( [ + 'xUserID' => $id, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNull( $info ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User and session key, with forceHTTPS flag + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + 'xToken' => $token, + 'forceHTTPS' => true, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertSame( $id, $info->getUserInfo()->getId() ); + $this->assertSame( $name, $info->getUserInfo()->getName() ); + $this->assertTrue( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Invalid user id + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => '-1', + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNull( $info ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User id with matching name + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + 'xUserName' => $name, + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNotNull( $info ); + $this->assertSame( $params['priority'], $info->getPriority() ); + $this->assertSame( $sessionId, $info->getId() ); + $this->assertNotNull( $info->getUserInfo() ); + $this->assertFalse( $info->getUserInfo()->isVerified() ); + $this->assertSame( $id, $info->getUserInfo()->getId() ); + $this->assertSame( $name, $info->getUserInfo()->getName() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + $logger->clearBuffer(); + + // User id with wrong name + $request = new \FauxRequest(); + $request->setCookies( [ + 'session' => $sessionId, + 'xUserID' => $id, + 'xUserName' => 'Wrong', + ], '' ); + $info = $provider->provideSessionInfo( $request ); + $this->assertNull( $info ); + $this->assertSame( [ + [ + LogLevel::WARNING, + 'Session "{session}" requested with mismatched UserID and UserName cookies.', + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + } + + public function testGetVaryCookies() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'cookieOptions' => [ 'prefix' => 'MyCookiePrefix' ], + ] ); + $this->assertArrayEquals( [ + 'MyCookiePrefixToken', + 'MyCookiePrefixLoggedOut', + 'MySessionName', + 'forceHTTPS', + ], $provider->getVaryCookies() ); + } + + public function testSuggestLoginUsername() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + + $request = new \FauxRequest(); + $this->assertEquals( null, $provider->suggestLoginUsername( $request ) ); + + $request->setCookies( [ + 'xUserName' => 'Example', + ], '' ); + $this->assertEquals( 'Example', $provider->suggestLoginUsername( $request ) ); + } + + public function testPersistSession() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'callUserSetCookiesHook' => false, + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + $config = $this->getConfig(); + $provider->setLogger( new \TestLogger() ); + $provider->setConfig( $config ); + $provider->setManager( SessionManager::singleton() ); + + $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $store = new TestBagOStuff(); + $user = static::getTestSysop()->getUser(); + $anon = new User; + + $backend = new SessionBackend( + new SessionId( $sessionId ), + new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $sessionId, + 'persisted' => true, + 'idIsSafe' => true, + ] ), + $store, + new \Psr\Log\NullLogger(), + 10 + ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; + + $mock = $this->getMockBuilder( stdClass::class ) + ->setMethods( [ 'onUserSetCookies' ] ) + ->getMock(); + $mock->expects( $this->never() )->method( 'onUserSetCookies' ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserSetCookies' => [ $mock ] ] ); + + // Anonymous user + $backend->setUser( $anon ); + $backend->setRememberUser( true ); + $backend->setForceHTTPS( false ); + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( [], $backend->getData() ); + + // Logged-in user, no remember + $backend->setUser( $user ); + $backend->setRememberUser( false ); + $backend->setForceHTTPS( false ); + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( [], $backend->getData() ); + + // Logged-in user, remember + $backend->setUser( $user ); + $backend->setRememberUser( true ); + $backend->setForceHTTPS( true ); + $request = new \FauxRequest(); + $time = time(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( [], $backend->getData() ); + } + + /** + * @dataProvider provideCookieData + * @param bool $secure + * @param bool $remember + */ + public function testCookieData( $secure, $remember ) { + $this->setMwGlobals( [ + 'wgSecureLogin' => false, + ] ); + + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'callUserSetCookiesHook' => false, + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + $config = $this->getConfig(); + $config->set( 'CookieSecure', $secure ); + $provider->setLogger( new \TestLogger() ); + $provider->setConfig( $config ); + $provider->setManager( SessionManager::singleton() ); + + $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $user = static::getTestSysop()->getUser(); + $this->assertFalse( $user->requiresHTTPS(), 'sanity check' ); + + $backend = new SessionBackend( + new SessionId( $sessionId ), + new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $sessionId, + 'persisted' => true, + 'idIsSafe' => true, + ] ), + new TestBagOStuff(), + new \Psr\Log\NullLogger(), + 10 + ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; + $backend->setUser( $user ); + $backend->setRememberUser( $remember ); + $backend->setForceHTTPS( $secure ); + $request = new \FauxRequest(); + $time = time(); + $provider->persistSession( $backend, $request ); + + $defaults = [ + 'expire' => (int)100, + 'path' => $config->get( 'CookiePath' ), + 'domain' => $config->get( 'CookieDomain' ), + 'secure' => $secure, + 'httpOnly' => $config->get( 'CookieHttpOnly' ), + 'raw' => false, + ]; + + $normalExpiry = $config->get( 'CookieExpiration' ); + $extendedExpiry = $config->get( 'ExtendedLoginCookieExpiration' ); + $extendedExpiry = (int)( $extendedExpiry === null ? 0 : $extendedExpiry ); + $expect = [ + 'MySessionName' => [ + 'value' => (string)$sessionId, + 'expire' => 0, + ] + $defaults, + 'xUserID' => [ + 'value' => (string)$user->getId(), + 'expire' => $remember ? $extendedExpiry : $normalExpiry, + ] + $defaults, + 'xUserName' => [ + 'value' => $user->getName(), + 'expire' => $remember ? $extendedExpiry : $normalExpiry + ] + $defaults, + 'xToken' => [ + 'value' => $remember ? $user->getToken() : '', + 'expire' => $remember ? $extendedExpiry : -31536000, + ] + $defaults, + 'forceHTTPS' => [ + 'value' => $secure ? 'true' : '', + 'secure' => false, + 'expire' => $secure ? $remember ? $defaults['expire'] : 0 : -31536000, + ] + $defaults, + ]; + foreach ( $expect as $key => $value ) { + $actual = $request->response()->getCookieData( $key ); + if ( $actual && $actual['expire'] > 0 ) { + // Round expiry so we don't randomly fail if the seconds ticked during the test. + $actual['expire'] = round( $actual['expire'] - $time, -2 ); + } + $this->assertEquals( $value, $actual, "Cookie $key" ); + } + } + + public static function provideCookieData() { + return [ + [ false, false ], + [ false, true ], + [ true, false ], + [ true, true ], + ]; + } + + protected function getSentRequest() { + $sentResponse = $this->getMockBuilder( \FauxResponse::class ) + ->setMethods( [ 'headersSent', 'setCookie', 'header' ] )->getMock(); + $sentResponse->expects( $this->any() )->method( 'headersSent' ) + ->will( $this->returnValue( true ) ); + $sentResponse->expects( $this->never() )->method( 'setCookie' ); + $sentResponse->expects( $this->never() )->method( 'header' ); + + $sentRequest = $this->getMockBuilder( \FauxRequest::class ) + ->setMethods( [ 'response' ] )->getMock(); + $sentRequest->expects( $this->any() )->method( 'response' ) + ->will( $this->returnValue( $sentResponse ) ); + return $sentRequest; + } + + public function testPersistSessionWithHook() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'callUserSetCookiesHook' => true, + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $this->getConfig() ); + $provider->setManager( SessionManager::singleton() ); + + $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $store = new TestBagOStuff(); + $user = static::getTestSysop()->getUser(); + $anon = new User; + + $backend = new SessionBackend( + new SessionId( $sessionId ), + new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $sessionId, + 'persisted' => true, + 'idIsSafe' => true, + ] ), + $store, + new \Psr\Log\NullLogger(), + 10 + ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; + + // Anonymous user + $mock = $this->getMockBuilder( stdClass::class ) + ->setMethods( [ 'onUserSetCookies' ] )->getMock(); + $mock->expects( $this->never() )->method( 'onUserSetCookies' ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserSetCookies' => [ $mock ] ] ); + $backend->setUser( $anon ); + $backend->setRememberUser( true ); + $backend->setForceHTTPS( false ); + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( [], $backend->getData() ); + + $provider->persistSession( $backend, $this->getSentRequest() ); + + // Logged-in user, no remember + $mock = $this->getMockBuilder( __CLASS__ ) + ->setMethods( [ 'onUserSetCookies' ] )->getMock(); + $mock->expects( $this->once() )->method( 'onUserSetCookies' ) + ->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $user ) { + $this->assertSame( $user, $u ); + $this->assertEquals( [ + 'wsUserID' => $user->getId(), + 'wsUserName' => $user->getName(), + 'wsToken' => $user->getToken(), + ], $sessionData ); + $this->assertEquals( [ + 'UserID' => $user->getId(), + 'UserName' => $user->getName(), + 'Token' => false, + ], $cookies ); + + $sessionData['foo'] = 'foo!'; + $cookies['bar'] = 'bar!'; + return true; + } ) ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserSetCookies' => [ $mock ] ] ); + $backend->setUser( $user ); + $backend->setRememberUser( false ); + $backend->setForceHTTPS( false ); + $backend->setLoggedOutTimestamp( $loggedOut = time() ); + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( 'bar!', $request->response()->getCookie( 'xbar' ) ); + $this->assertSame( (string)$loggedOut, $request->response()->getCookie( 'xLoggedOut' ) ); + $this->assertEquals( [ + 'wsUserID' => $user->getId(), + 'wsUserName' => $user->getName(), + 'wsToken' => $user->getToken(), + 'foo' => 'foo!', + ], $backend->getData() ); + + $provider->persistSession( $backend, $this->getSentRequest() ); + + // Logged-in user, remember + $mock = $this->getMockBuilder( __CLASS__ ) + ->setMethods( [ 'onUserSetCookies' ] )->getMock(); + $mock->expects( $this->once() )->method( 'onUserSetCookies' ) + ->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $user ) { + $this->assertSame( $user, $u ); + $this->assertEquals( [ + 'wsUserID' => $user->getId(), + 'wsUserName' => $user->getName(), + 'wsToken' => $user->getToken(), + ], $sessionData ); + $this->assertEquals( [ + 'UserID' => $user->getId(), + 'UserName' => $user->getName(), + 'Token' => $user->getToken(), + ], $cookies ); + + $sessionData['foo'] = 'foo 2!'; + $cookies['bar'] = 'bar 2!'; + return true; + } ) ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserSetCookies' => [ $mock ] ] ); + $backend->setUser( $user ); + $backend->setRememberUser( true ); + $backend->setForceHTTPS( true ); + $backend->setLoggedOutTimestamp( 0 ); + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) ); + $this->assertSame( 'bar 2!', $request->response()->getCookie( 'xbar' ) ); + $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) ); + $this->assertEquals( [ + 'wsUserID' => $user->getId(), + 'wsUserName' => $user->getName(), + 'wsToken' => $user->getToken(), + 'foo' => 'foo 2!', + ], $backend->getData() ); + + $provider->persistSession( $backend, $this->getSentRequest() ); + } + + public function testUnpersistSession() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $this->getConfig() ); + $provider->setManager( SessionManager::singleton() ); + + $request = new \FauxRequest(); + $provider->unpersistSession( $request ); + $this->assertSame( '', $request->response()->getCookie( 'MySessionName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xUserID' ) ); + $this->assertSame( null, $request->response()->getCookie( 'xUserName' ) ); + $this->assertSame( '', $request->response()->getCookie( 'xToken' ) ); + $this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) ); + + $provider->unpersistSession( $this->getSentRequest() ); + } + + public function testSetLoggedOutCookie() { + $provider = TestingAccessWrapper::newFromObject( new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ) ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $this->getConfig() ); + $provider->setManager( SessionManager::singleton() ); + + $t1 = time(); + $t2 = time() - 86400 * 2; + + // Set it + $request = new \FauxRequest(); + $provider->setLoggedOutCookie( $t1, $request ); + $this->assertSame( (string)$t1, $request->response()->getCookie( 'xLoggedOut' ) ); + + // Too old + $request = new \FauxRequest(); + $provider->setLoggedOutCookie( $t2, $request ); + $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) ); + + // Don't reset if it's already set + $request = new \FauxRequest(); + $request->setCookies( [ + 'xLoggedOut' => $t1, + ], '' ); + $provider->setLoggedOutCookie( $t1, $request ); + $this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) ); + } + + /** + * To be mocked for hooks, since PHPUnit can't otherwise mock methods that + * take references. + */ + public function onUserSetCookies( $user, &$sessionData, &$cookies ) { + } + + public function testGetCookie() { + $provider = new CookieSessionProvider( [ + 'priority' => 1, + 'sessionName' => 'MySessionName', + 'cookieOptions' => [ 'prefix' => 'x' ], + ] ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $this->getConfig() ); + $provider->setManager( SessionManager::singleton() ); + $provider = TestingAccessWrapper::newFromObject( $provider ); + + $request = new \FauxRequest(); + $request->setCookies( [ + 'xFoo' => 'foo!', + 'xBar' => 'deleted', + ], '' ); + $this->assertSame( 'foo!', $provider->getCookie( $request, 'Foo', 'x' ) ); + $this->assertNull( $provider->getCookie( $request, 'Bar', 'x' ) ); + $this->assertNull( $provider->getCookie( $request, 'Baz', 'x' ) ); + } + + public function testGetRememberUserDuration() { + $config = $this->getConfig(); + $provider = new CookieSessionProvider( [ 'priority' => 10 ] ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $config ); + $provider->setManager( SessionManager::singleton() ); + + $this->assertSame( 200, $provider->getRememberUserDuration() ); + + $config->set( 'ExtendedLoginCookieExpiration', null ); + + $this->assertSame( 100, $provider->getRememberUserDuration() ); + + $config->set( 'ExtendedLoginCookieExpiration', 0 ); + + $this->assertSame( null, $provider->getRememberUserDuration() ); + } + + public function testGetLoginCookieExpiration() { + $config = $this->getConfig(); + $provider = TestingAccessWrapper::newFromObject( new CookieSessionProvider( [ + 'priority' => 10 + ] ) ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $provider->setConfig( $config ); + $provider->setManager( SessionManager::singleton() ); + + // First cookie is an extended cookie, remember me true + $this->assertSame( 200, $provider->getLoginCookieExpiration( 'Token', true ) ); + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User', true ) ); + + // First cookie is an extended cookie, remember me false + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'UserID', false ) ); + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User', false ) ); + + $config->set( 'ExtendedLoginCookieExpiration', null ); + + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'Token', true ) ); + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User', true ) ); + + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'Token', false ) ); + $this->assertSame( 100, $provider->getLoginCookieExpiration( 'User', false ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php b/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php new file mode 100644 index 00000000..6dd32fcd --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/ImmutableSessionProviderWithCookieTest.php @@ -0,0 +1,305 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use User; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\ImmutableSessionProviderWithCookie + */ +class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase { + + private function getProvider( $name, $prefix = null ) { + $config = new \HashConfig(); + $config->set( 'CookiePrefix', 'wgCookiePrefix' ); + + $params = [ + 'sessionCookieName' => $name, + 'sessionCookieOptions' => [], + ]; + if ( $prefix !== null ) { + $params['sessionCookieOptions']['prefix'] = $prefix; + } + + $provider = $this->getMockBuilder( ImmutableSessionProviderWithCookie::class ) + ->setConstructorArgs( [ $params ] ) + ->getMockForAbstractClass(); + $provider->setLogger( new \TestLogger() ); + $provider->setConfig( $config ); + $provider->setManager( new SessionManager() ); + + return $provider; + } + + public function testConstructor() { + $provider = $this->getMockBuilder( ImmutableSessionProviderWithCookie::class ) + ->getMockForAbstractClass(); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $this->assertNull( $priv->sessionCookieName ); + $this->assertSame( [], $priv->sessionCookieOptions ); + + $provider = $this->getMockBuilder( ImmutableSessionProviderWithCookie::class ) + ->setConstructorArgs( [ [ + 'sessionCookieName' => 'Foo', + 'sessionCookieOptions' => [ 'Bar' ], + ] ] ) + ->getMockForAbstractClass(); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $this->assertSame( 'Foo', $priv->sessionCookieName ); + $this->assertSame( [ 'Bar' ], $priv->sessionCookieOptions ); + + try { + $provider = $this->getMockBuilder( ImmutableSessionProviderWithCookie::class ) + ->setConstructorArgs( [ [ + 'sessionCookieName' => false, + ] ] ) + ->getMockForAbstractClass(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'sessionCookieName must be a string', + $ex->getMessage() + ); + } + + try { + $provider = $this->getMockBuilder( ImmutableSessionProviderWithCookie::class ) + ->setConstructorArgs( [ [ + 'sessionCookieOptions' => 'x', + ] ] ) + ->getMockForAbstractClass(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'sessionCookieOptions must be an array', + $ex->getMessage() + ); + } + } + + public function testBasics() { + $provider = $this->getProvider( null ); + $this->assertFalse( $provider->persistsSessionID() ); + $this->assertFalse( $provider->canChangeUser() ); + + $provider = $this->getProvider( 'Foo' ); + $this->assertTrue( $provider->persistsSessionID() ); + $this->assertFalse( $provider->canChangeUser() ); + + $msg = $provider->whyNoSession(); + $this->assertInstanceOf( \Message::class, $msg ); + $this->assertSame( 'sessionprovider-nocookies', $msg->getKey() ); + } + + public function testGetVaryCookies() { + $provider = $this->getProvider( null ); + $this->assertSame( [], $provider->getVaryCookies() ); + + $provider = $this->getProvider( 'Foo' ); + $this->assertSame( [ 'wgCookiePrefixFoo' ], $provider->getVaryCookies() ); + + $provider = $this->getProvider( 'Foo', 'Bar' ); + $this->assertSame( [ 'BarFoo' ], $provider->getVaryCookies() ); + + $provider = $this->getProvider( 'Foo', '' ); + $this->assertSame( [ 'Foo' ], $provider->getVaryCookies() ); + } + + public function testGetSessionIdFromCookie() { + $this->setMwGlobals( 'wgCookiePrefix', 'wgCookiePrefix' ); + $request = new \FauxRequest(); + $request->setCookies( [ + '' => 'empty---------------------------', + 'Foo' => 'foo-----------------------------', + 'wgCookiePrefixFoo' => 'wgfoo---------------------------', + 'BarFoo' => 'foobar--------------------------', + 'bad' => 'bad', + ], '' ); + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( null ) ); + try { + $provider->getSessionIdFromCookie( $request ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \BadMethodCallException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie::getSessionIdFromCookie ' . + 'may not be called when $this->sessionCookieName === null', + $ex->getMessage() + ); + } + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo' ) ); + $this->assertSame( + 'wgfoo---------------------------', + $provider->getSessionIdFromCookie( $request ) + ); + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', 'Bar' ) ); + $this->assertSame( + 'foobar--------------------------', + $provider->getSessionIdFromCookie( $request ) + ); + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', '' ) ); + $this->assertSame( + 'foo-----------------------------', + $provider->getSessionIdFromCookie( $request ) + ); + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( 'bad', '' ) ); + $this->assertSame( null, $provider->getSessionIdFromCookie( $request ) ); + + $provider = TestingAccessWrapper::newFromObject( $this->getProvider( 'none', '' ) ); + $this->assertSame( null, $provider->getSessionIdFromCookie( $request ) ); + } + + protected function getSentRequest() { + $sentResponse = $this->getMockBuilder( \FauxResponse::class ) + ->setMethods( [ 'headersSent', 'setCookie', 'header' ] ) + ->getMock(); + $sentResponse->expects( $this->any() )->method( 'headersSent' ) + ->will( $this->returnValue( true ) ); + $sentResponse->expects( $this->never() )->method( 'setCookie' ); + $sentResponse->expects( $this->never() )->method( 'header' ); + + $sentRequest = $this->getMockBuilder( \FauxRequest::class ) + ->setMethods( [ 'response' ] )->getMock(); + $sentRequest->expects( $this->any() )->method( 'response' ) + ->will( $this->returnValue( $sentResponse ) ); + return $sentRequest; + } + + /** + * @dataProvider providePersistSession + * @param bool $secure + * @param bool $remember + */ + public function testPersistSession( $secure, $remember ) { + $this->setMwGlobals( [ + 'wgCookieExpiration' => 100, + 'wgSecureLogin' => false, + ] ); + + $provider = $this->getProvider( 'session' ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + $priv->sessionCookieOptions = [ + 'prefix' => 'x', + 'path' => 'CookiePath', + 'domain' => 'CookieDomain', + 'secure' => false, + 'httpOnly' => true, + ]; + + $sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $user = User::newFromName( 'UTSysop' ); + $this->assertFalse( $user->requiresHTTPS(), 'sanity check' ); + + $backend = new SessionBackend( + new SessionId( $sessionId ), + new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $sessionId, + 'persisted' => true, + 'userInfo' => UserInfo::newFromUser( $user, true ), + 'idIsSafe' => true, + ] ), + new TestBagOStuff(), + new \Psr\Log\NullLogger(), + 10 + ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false; + $backend->setRememberUser( $remember ); + $backend->setForceHTTPS( $secure ); + + // No cookie + $priv->sessionCookieName = null; + $request = new \FauxRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( [], $request->response()->getCookies() ); + + // Cookie + $priv->sessionCookieName = 'session'; + $request = new \FauxRequest(); + $time = time(); + $provider->persistSession( $backend, $request ); + + $cookie = $request->response()->getCookieData( 'xsession' ); + $this->assertInternalType( 'array', $cookie ); + if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) { + // Round expiry so we don't randomly fail if the seconds ticked during the test. + $cookie['expire'] = round( $cookie['expire'] - $time, -2 ); + } + $this->assertEquals( [ + 'value' => $sessionId, + 'expire' => null, + 'path' => 'CookiePath', + 'domain' => 'CookieDomain', + 'secure' => $secure, + 'httpOnly' => true, + 'raw' => false, + ], $cookie ); + + $cookie = $request->response()->getCookieData( 'forceHTTPS' ); + if ( $secure ) { + $this->assertInternalType( 'array', $cookie ); + if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) { + // Round expiry so we don't randomly fail if the seconds ticked during the test. + $cookie['expire'] = round( $cookie['expire'] - $time, -2 ); + } + $this->assertEquals( [ + 'value' => 'true', + 'expire' => null, + 'path' => 'CookiePath', + 'domain' => 'CookieDomain', + 'secure' => false, + 'httpOnly' => true, + 'raw' => false, + ], $cookie ); + } else { + $this->assertNull( $cookie ); + } + + // Headers sent + $request = $this->getSentRequest(); + $provider->persistSession( $backend, $request ); + $this->assertSame( [], $request->response()->getCookies() ); + } + + public static function providePersistSession() { + return [ + [ false, false ], + [ false, true ], + [ true, false ], + [ true, true ], + ]; + } + + public function testUnpersistSession() { + $provider = $this->getProvider( 'session', '' ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + + // No cookie + $priv->sessionCookieName = null; + $request = new \FauxRequest(); + $provider->unpersistSession( $request ); + $this->assertSame( null, $request->response()->getCookie( 'session', '' ) ); + + // Cookie + $priv->sessionCookieName = 'session'; + $request = new \FauxRequest(); + $provider->unpersistSession( $request ); + $this->assertSame( '', $request->response()->getCookie( 'session', '' ) ); + + // Headers sent + $request = $this->getSentRequest(); + $provider->unpersistSession( $request ); + $this->assertSame( null, $request->response()->getCookie( 'session', '' ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php b/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php new file mode 100644 index 00000000..8cb4302a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/MetadataMergeExceptionTest.php @@ -0,0 +1,30 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; + +/** + * @group Session + * @covers MediaWiki\Session\MetadataMergeException + */ +class MetadataMergeExceptionTest extends MediaWikiTestCase { + + public function testBasics() { + $data = [ 'foo' => 'bar' ]; + + $ex = new MetadataMergeException(); + $this->assertInstanceOf( \UnexpectedValueException::class, $ex ); + $this->assertSame( [], $ex->getContext() ); + + $ex2 = new MetadataMergeException( 'Message', 42, $ex, $data ); + $this->assertSame( 'Message', $ex2->getMessage() ); + $this->assertSame( 42, $ex2->getCode() ); + $this->assertSame( $ex, $ex2->getPrevious() ); + $this->assertSame( $data, $ex2->getContext() ); + + $ex->setContext( $data ); + $this->assertSame( $data, $ex->getContext() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php b/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php new file mode 100644 index 00000000..045ba2f0 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/PHPSessionHandlerTest.php @@ -0,0 +1,361 @@ +<?php + +namespace MediaWiki\Session; + +use Psr\Log\LogLevel; +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @covers MediaWiki\Session\PHPSessionHandler + */ +class PHPSessionHandlerTest extends MediaWikiTestCase { + + private function getResetter( &$rProp = null ) { + $reset = []; + + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + if ( $rProp->getValue() ) { + $old = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $oldManager = $old->manager; + $oldStore = $old->store; + $oldLogger = $old->logger; + $reset[] = new \Wikimedia\ScopedCallback( + [ PHPSessionHandler::class, 'install' ], + [ $oldManager, $oldStore, $oldLogger ] + ); + } + + return $reset; + } + + public function testEnableFlags() { + $handler = TestingAccessWrapper::newFromObject( + $this->getMockBuilder( PHPSessionHandler::class ) + ->setMethods( null ) + ->disableOriginalConstructor() + ->getMock() + ); + + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $reset = new \Wikimedia\ScopedCallback( [ $rProp, 'setValue' ], [ $rProp->getValue() ] ); + $rProp->setValue( $handler ); + + $handler->setEnableFlags( 'enable' ); + $this->assertTrue( $handler->enable ); + $this->assertFalse( $handler->warn ); + $this->assertTrue( PHPSessionHandler::isEnabled() ); + + $handler->setEnableFlags( 'warn' ); + $this->assertTrue( $handler->enable ); + $this->assertTrue( $handler->warn ); + $this->assertTrue( PHPSessionHandler::isEnabled() ); + + $handler->setEnableFlags( 'disable' ); + $this->assertFalse( $handler->enable ); + $this->assertFalse( PHPSessionHandler::isEnabled() ); + + $rProp->setValue( null ); + $this->assertFalse( PHPSessionHandler::isEnabled() ); + } + + public function testInstall() { + $reset = $this->getResetter( $rProp ); + $rProp->setValue( null ); + + session_write_close(); + ini_set( 'session.use_cookies', 1 ); + ini_set( 'session.use_trans_sid', 1 ); + + $store = new TestBagOStuff(); + $logger = new \TestLogger(); + $manager = new SessionManager( [ + 'store' => $store, + 'logger' => $logger, + ] ); + + $this->assertFalse( PHPSessionHandler::isInstalled() ); + PHPSessionHandler::install( $manager ); + $this->assertTrue( PHPSessionHandler::isInstalled() ); + + $this->assertFalse( wfIniGetBool( 'session.use_cookies' ) ); + $this->assertFalse( wfIniGetBool( 'session.use_trans_sid' ) ); + + $this->assertNotNull( $rProp->getValue() ); + $priv = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $this->assertSame( $manager, $priv->manager ); + $this->assertSame( $store, $priv->store ); + $this->assertSame( $logger, $priv->logger ); + } + + /** + * @dataProvider provideHandlers + * @param string $handler php serialize_handler to use + */ + public function testSessionHandling( $handler ) { + $this->hideDeprecated( '$_SESSION' ); + $reset[] = $this->getResetter( $rProp ); + + $this->setMwGlobals( [ + 'wgSessionProviders' => [ [ 'class' => \DummySessionProvider::class ] ], + 'wgObjectCacheSessionExpiry' => 2, + ] ); + + $store = new TestBagOStuff(); + $logger = new \TestLogger( true, function ( $m ) { + // Discard all log events starting with expected prefix + return preg_match( '/^SessionBackend "\{session\}" /', $m ) ? null : $m; + } ); + $manager = new SessionManager( [ + 'store' => $store, + 'logger' => $logger, + ] ); + PHPSessionHandler::install( $manager ); + $wrap = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $reset[] = new \Wikimedia\ScopedCallback( + [ $wrap, 'setEnableFlags' ], + [ $wrap->enable ? $wrap->warn ? 'warn' : 'enable' : 'disable' ] + ); + $wrap->setEnableFlags( 'warn' ); + + \Wikimedia\suppressWarnings(); + ini_set( 'session.serialize_handler', $handler ); + \Wikimedia\restoreWarnings(); + if ( ini_get( 'session.serialize_handler' ) !== $handler ) { + $this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" ); + } + + // Session IDs for testing + $sessionA = str_repeat( 'a', 32 ); + $sessionB = str_repeat( 'b', 32 ); + $sessionC = str_repeat( 'c', 32 ); + + // Set up garbage data in the session + $_SESSION['AuthenticationSessionTest'] = 'bogus'; + + session_id( $sessionA ); + session_start(); + $this->assertSame( [], $_SESSION ); + $this->assertSame( $sessionA, session_id() ); + + // Set some data in the session so we can see if it works. + $rand = mt_rand(); + $_SESSION['AuthenticationSessionTest'] = $rand; + $expect = [ 'AuthenticationSessionTest' => $rand ]; + session_write_close(); + $this->assertSame( [ + [ LogLevel::WARNING, 'Something wrote to $_SESSION!' ], + ], $logger->getBuffer() ); + + // Screw up $_SESSION so we can tell the difference between "this + // worked" and "this did nothing" + $_SESSION['AuthenticationSessionTest'] = 'bogus'; + + // Re-open the session and see that data was actually reloaded + session_start(); + $this->assertSame( $expect, $_SESSION ); + + // Make sure session_reset() works too. + if ( function_exists( 'session_reset' ) ) { + $_SESSION['AuthenticationSessionTest'] = 'bogus'; + session_reset(); + $this->assertSame( $expect, $_SESSION ); + } + + // Re-fill the session, then test that session_destroy() works. + $_SESSION['AuthenticationSessionTest'] = $rand; + session_write_close(); + session_start(); + $this->assertSame( $expect, $_SESSION ); + session_destroy(); + session_id( $sessionA ); + session_start(); + $this->assertSame( [], $_SESSION ); + session_write_close(); + + // Test that our session handler won't clone someone else's session + session_id( $sessionB ); + session_start(); + $this->assertSame( $sessionB, session_id() ); + $_SESSION['id'] = 'B'; + session_write_close(); + + session_id( $sessionC ); + session_start(); + $this->assertSame( [], $_SESSION ); + $_SESSION['id'] = 'C'; + session_write_close(); + + session_id( $sessionB ); + session_start(); + $this->assertSame( [ 'id' => 'B' ], $_SESSION ); + session_write_close(); + + session_id( $sessionC ); + session_start(); + $this->assertSame( [ 'id' => 'C' ], $_SESSION ); + session_destroy(); + + session_id( $sessionB ); + session_start(); + $this->assertSame( [ 'id' => 'B' ], $_SESSION ); + + // Test merging between Session and $_SESSION + session_write_close(); + + $session = $manager->getEmptySession(); + $session->set( 'Unchanged', 'setup' ); + $session->set( 'Unchanged, null', null ); + $session->set( 'Changed in $_SESSION', 'setup' ); + $session->set( 'Changed in Session', 'setup' ); + $session->set( 'Changed in both', 'setup' ); + $session->set( 'Deleted in Session', 'setup' ); + $session->set( 'Deleted in $_SESSION', 'setup' ); + $session->set( 'Deleted in both', 'setup' ); + $session->set( 'Deleted in Session, changed in $_SESSION', 'setup' ); + $session->set( 'Deleted in $_SESSION, changed in Session', 'setup' ); + $session->persist(); + $session->save(); + + session_id( $session->getId() ); + session_start(); + $session->set( 'Added in Session', 'Session' ); + $session->set( 'Added in both', 'Session' ); + $session->set( 'Changed in Session', 'Session' ); + $session->set( 'Changed in both', 'Session' ); + $session->set( 'Deleted in $_SESSION, changed in Session', 'Session' ); + $session->remove( 'Deleted in Session' ); + $session->remove( 'Deleted in both' ); + $session->remove( 'Deleted in Session, changed in $_SESSION' ); + $session->save(); + $_SESSION['Added in $_SESSION'] = '$_SESSION'; + $_SESSION['Added in both'] = '$_SESSION'; + $_SESSION['Changed in $_SESSION'] = '$_SESSION'; + $_SESSION['Changed in both'] = '$_SESSION'; + $_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION'; + unset( $_SESSION['Deleted in $_SESSION'] ); + unset( $_SESSION['Deleted in both'] ); + unset( $_SESSION['Deleted in $_SESSION, changed in Session'] ); + session_write_close(); + + $this->assertEquals( [ + 'Added in Session' => 'Session', + 'Added in $_SESSION' => '$_SESSION', + 'Added in both' => 'Session', + 'Unchanged' => 'setup', + 'Unchanged, null' => null, + 'Changed in Session' => 'Session', + 'Changed in $_SESSION' => '$_SESSION', + 'Changed in both' => 'Session', + 'Deleted in Session, changed in $_SESSION' => '$_SESSION', + 'Deleted in $_SESSION, changed in Session' => 'Session', + ], iterator_to_array( $session ) ); + + $session->clear(); + $session->set( 42, 'forty-two' ); + $session->set( 'forty-two', 42 ); + $session->set( 'wrong', 43 ); + $session->persist(); + $session->save(); + + session_start(); + $this->assertArrayHasKey( 'forty-two', $_SESSION ); + $this->assertSame( 42, $_SESSION['forty-two'] ); + $this->assertArrayHasKey( 'wrong', $_SESSION ); + unset( $_SESSION['wrong'] ); + session_write_close(); + + $this->assertEquals( [ + 42 => 'forty-two', + 'forty-two' => 42, + ], iterator_to_array( $session ) ); + + // Test that write doesn't break if the session is invalid + $session = $manager->getEmptySession(); + $session->persist(); + $id = $session->getId(); + unset( $session ); + session_id( $id ); + session_start(); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'SessionCheckInfo' => [ function ( &$reason ) { + $reason = 'Testing'; + return false; + } ], + ] ); + $this->assertNull( $manager->getSessionById( $id, true ), 'sanity check' ); + session_write_close(); + + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'SessionCheckInfo' => [], + ] ); + $this->assertNotNull( $manager->getSessionById( $id, true ), 'sanity check' ); + } + + public static function provideHandlers() { + return [ + [ 'php' ], + [ 'php_binary' ], + [ 'php_serialize' ], + ]; + } + + /** + * @dataProvider provideDisabled + * @expectedException BadMethodCallException + * @expectedExceptionMessage Attempt to use PHP session management + */ + public function testDisabled( $method, $args ) { + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $handler = $this->getMockBuilder( PHPSessionHandler::class ) + ->setMethods( null ) + ->disableOriginalConstructor() + ->getMock(); + TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' ); + $oldValue = $rProp->getValue(); + $rProp->setValue( $handler ); + $reset = new \Wikimedia\ScopedCallback( [ $rProp, 'setValue' ], [ $oldValue ] ); + + call_user_func_array( [ $handler, $method ], $args ); + } + + public static function provideDisabled() { + return [ + [ 'open', [ '', '' ] ], + [ 'read', [ '' ] ], + [ 'write', [ '', '' ] ], + [ 'destroy', [ '' ] ], + ]; + } + + /** + * @dataProvider provideWrongInstance + * @expectedException UnexpectedValueException + * @expectedExceptionMessageRegExp /: Wrong instance called!$/ + */ + public function testWrongInstance( $method, $args ) { + $handler = $this->getMockBuilder( PHPSessionHandler::class ) + ->setMethods( null ) + ->disableOriginalConstructor() + ->getMock(); + TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' ); + + call_user_func_array( [ $handler, $method ], $args ); + } + + public static function provideWrongInstance() { + return [ + [ 'open', [ '', '' ] ], + [ 'close', [] ], + [ 'read', [ '' ] ], + [ 'write', [ '', '' ] ], + [ 'destroy', [ '' ] ], + [ 'gc', [ 0 ] ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php b/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php new file mode 100644 index 00000000..48c3d179 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionBackendTest.php @@ -0,0 +1,963 @@ +<?php + +namespace MediaWiki\Session; + +use Config; +use MediaWikiTestCase; +use User; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\SessionBackend + */ +class SessionBackendTest extends MediaWikiTestCase { + const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + + /** @var SessionManager */ + protected $manager; + + /** @var Config */ + protected $config; + + /** @var SessionProvider */ + protected $provider; + + /** @var TestBagOStuff */ + protected $store; + + protected $onSessionMetadataCalled = false; + + /** + * Returns a non-persistent backend that thinks it has at least one session active + * @param User|null $user + * @param string $id + * @return SessionBackend + */ + protected function getBackend( User $user = null, $id = null ) { + if ( !$this->config ) { + $this->config = new \HashConfig(); + $this->manager = null; + } + if ( !$this->store ) { + $this->store = new TestBagOStuff(); + $this->manager = null; + } + + $logger = new \Psr\Log\NullLogger(); + if ( !$this->manager ) { + $this->manager = new SessionManager( [ + 'store' => $this->store, + 'logger' => $logger, + 'config' => $this->config, + ] ); + } + + if ( !$this->provider ) { + $this->provider = new \DummySessionProvider(); + } + $this->provider->setLogger( $logger ); + $this->provider->setConfig( $this->config ); + $this->provider->setManager( $this->manager ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $this->provider, + 'id' => $id ?: self::SESSIONID, + 'persisted' => true, + 'userInfo' => UserInfo::newFromUser( $user ?: new User, true ), + 'idIsSafe' => true, + ] ); + $id = new SessionId( $info->getId() ); + + $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $priv = TestingAccessWrapper::newFromObject( $backend ); + $priv->persist = false; + $priv->requests = [ 100 => new \FauxRequest() ]; + $priv->requests[100]->setSessionId( $id ); + $priv->usePhpSessionHandling = false; + + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $manager->allSessionBackends = [ $backend->getId() => $backend ] + $manager->allSessionBackends; + $manager->allSessionIds = [ $backend->getId() => $id ] + $manager->allSessionIds; + $manager->sessionProviders = [ (string)$this->provider => $this->provider ]; + + return $backend; + } + + public function testConstructor() { + // Set variables + $this->getBackend(); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $this->provider, + 'id' => self::SESSIONID, + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', false ), + 'idIsSafe' => true, + ] ); + $id = new SessionId( $info->getId() ); + $logger = new \Psr\Log\NullLogger(); + try { + new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + "Refusing to create session for unverified user {$info->getUserInfo()}", + $ex->getMessage() + ); + } + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => self::SESSIONID, + 'userInfo' => UserInfo::newFromName( 'UTSysop', true ), + 'idIsSafe' => true, + ] ); + $id = new SessionId( $info->getId() ); + try { + new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() ); + } + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $this->provider, + 'id' => self::SESSIONID, + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', true ), + 'idIsSafe' => true, + ] ); + $id = new SessionId( '!' . $info->getId() ); + try { + new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'SessionId and SessionInfo don\'t match', + $ex->getMessage() + ); + } + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $this->provider, + 'id' => self::SESSIONID, + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', true ), + 'idIsSafe' => true, + ] ); + $id = new SessionId( $info->getId() ); + $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $this->assertSame( self::SESSIONID, $backend->getId() ); + $this->assertSame( $id, $backend->getSessionId() ); + $this->assertSame( $this->provider, $backend->getProvider() ); + $this->assertInstanceOf( User::class, $backend->getUser() ); + $this->assertSame( 'UTSysop', $backend->getUser()->getName() ); + $this->assertSame( $info->wasPersisted(), $backend->isPersistent() ); + $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() ); + $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() ); + + $expire = time() + 100; + $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ] ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $this->provider, + 'id' => self::SESSIONID, + 'persisted' => true, + 'forceHTTPS' => true, + 'metadata' => [ 'foo' ], + 'idIsSafe' => true, + ] ); + $id = new SessionId( $info->getId() ); + $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 ); + $this->assertSame( self::SESSIONID, $backend->getId() ); + $this->assertSame( $id, $backend->getSessionId() ); + $this->assertSame( $this->provider, $backend->getProvider() ); + $this->assertInstanceOf( User::class, $backend->getUser() ); + $this->assertTrue( $backend->getUser()->isAnon() ); + $this->assertSame( $info->wasPersisted(), $backend->isPersistent() ); + $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() ); + $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() ); + $this->assertSame( $expire, TestingAccessWrapper::newFromObject( $backend )->expires ); + $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() ); + } + + public function testSessionStuff() { + $backend = $this->getBackend(); + $priv = TestingAccessWrapper::newFromObject( $backend ); + $priv->requests = []; // Remove dummy session + + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + + $request1 = new \FauxRequest(); + $session1 = $backend->getSession( $request1 ); + $request2 = new \FauxRequest(); + $session2 = $backend->getSession( $request2 ); + + $this->assertInstanceOf( Session::class, $session1 ); + $this->assertInstanceOf( Session::class, $session2 ); + $this->assertSame( 2, count( $priv->requests ) ); + + $index = TestingAccessWrapper::newFromObject( $session1 )->index; + + $this->assertSame( $request1, $backend->getRequest( $index ) ); + $this->assertSame( null, $backend->suggestLoginUsername( $index ) ); + $request1->setCookie( 'UserName', 'Example' ); + $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) ); + + $session1 = null; + $this->assertSame( 1, count( $priv->requests ) ); + $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends ); + $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] ); + try { + $backend->getRequest( $index ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid session index', $ex->getMessage() ); + } + try { + $backend->suggestLoginUsername( $index ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid session index', $ex->getMessage() ); + } + + $session2 = null; + $this->assertSame( 0, count( $priv->requests ) ); + $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends ); + $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds ); + } + + public function testSetProviderMetadata() { + $backend = $this->getBackend(); + $priv = TestingAccessWrapper::newFromObject( $backend ); + $priv->providerMetadata = [ 'dummy' ]; + + try { + $backend->setProviderMetadata( 'foo' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( '$metadata must be an array or null', $ex->getMessage() ); + } + + try { + $backend->setProviderMetadata( (object)[] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( '$metadata must be an array or null', $ex->getMessage() ); + } + + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' ); + $backend->setProviderMetadata( [ 'dummy' ] ); + $this->assertFalse( $this->store->getSession( self::SESSIONID ) ); + + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' ); + $backend->setProviderMetadata( [ 'test' ] ); + $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) ); + $this->assertSame( [ 'test' ], $backend->getProviderMetadata() ); + $this->store->deleteSession( self::SESSIONID ); + + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' ); + $backend->setProviderMetadata( null ); + $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) ); + $this->assertSame( null, $backend->getProviderMetadata() ); + $this->store->deleteSession( self::SESSIONID ); + } + + public function testResetId() { + $id = session_id(); + + $builder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] ); + + $this->provider = $builder->getMock(); + $this->provider->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( false ) ); + $this->provider->expects( $this->never() )->method( 'sessionIdWasReset' ); + $backend = $this->getBackend( User::newFromName( 'UTSysop' ) ); + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $sessionId = $backend->getSessionId(); + $backend->resetId(); + $this->assertSame( self::SESSIONID, $backend->getId() ); + $this->assertSame( $backend->getId(), $sessionId->getId() ); + $this->assertSame( $id, session_id() ); + $this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] ); + + $this->provider = $builder->getMock(); + $this->provider->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( true ) ); + $backend = $this->getBackend(); + $this->provider->expects( $this->once() )->method( 'sessionIdWasReset' ) + ->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) ); + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $sessionId = $backend->getSessionId(); + $backend->resetId(); + $this->assertNotEquals( self::SESSIONID, $backend->getId() ); + $this->assertSame( $backend->getId(), $sessionId->getId() ); + $this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) ); + $this->assertFalse( $this->store->getSession( self::SESSIONID ) ); + $this->assertSame( $id, session_id() ); + $this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends ); + $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends ); + $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] ); + } + + public function testPersist() { + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistSession' ] )->getMock(); + $this->provider->expects( $this->once() )->method( 'persistSession' ); + $backend = $this->getBackend(); + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + $backend->save(); // This one shouldn't call $provider->persistSession() + + $backend->persist(); + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + + $this->provider = null; + $backend = $this->getBackend(); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $wrap->persist = true; + $wrap->expires = 0; + $backend->persist(); + $this->assertNotEquals( 0, $wrap->expires ); + } + + public function testUnpersist() { + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'unpersistSession' ] )->getMock(); + $this->provider->expects( $this->once() )->method( 'unpersistSession' ); + $backend = $this->getBackend(); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $wrap->store = new \CachedBagOStuff( $this->store ); + $wrap->persist = true; + $wrap->dataDirty = true; + + $backend->save(); // This one shouldn't call $provider->persistSession(), but should save + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + $this->assertNotFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' ); + + $backend->unpersist(); + $this->assertFalse( $backend->isPersistent() ); + $this->assertFalse( $this->store->getSession( self::SESSIONID ) ); + $this->assertNotFalse( + $wrap->store->get( $wrap->store->makeKey( 'MWSession', self::SESSIONID ) ) + ); + } + + public function testRememberUser() { + $backend = $this->getBackend(); + + $remembered = $backend->shouldRememberUser(); + $backend->setRememberUser( !$remembered ); + $this->assertNotEquals( $remembered, $backend->shouldRememberUser() ); + $backend->setRememberUser( $remembered ); + $this->assertEquals( $remembered, $backend->shouldRememberUser() ); + } + + public function testForceHTTPS() { + $backend = $this->getBackend(); + + $force = $backend->shouldForceHTTPS(); + $backend->setForceHTTPS( !$force ); + $this->assertNotEquals( $force, $backend->shouldForceHTTPS() ); + $backend->setForceHTTPS( $force ); + $this->assertEquals( $force, $backend->shouldForceHTTPS() ); + } + + public function testLoggedOutTimestamp() { + $backend = $this->getBackend(); + + $backend->setLoggedOutTimestamp( 42 ); + $this->assertSame( 42, $backend->getLoggedOutTimestamp() ); + $backend->setLoggedOutTimestamp( '123' ); + $this->assertSame( 123, $backend->getLoggedOutTimestamp() ); + } + + public function testSetUser() { + $user = static::getTestSysop()->getUser(); + + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'canChangeUser' ] )->getMock(); + $this->provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( false ) ); + $backend = $this->getBackend(); + $this->assertFalse( $backend->canSetUser() ); + try { + $backend->setUser( $user ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \BadMethodCallException $ex ) { + $this->assertSame( + 'Cannot set user on this session; check $session->canSetUser() first', + $ex->getMessage() + ); + } + $this->assertNotSame( $user, $backend->getUser() ); + + $this->provider = null; + $backend = $this->getBackend(); + $this->assertTrue( $backend->canSetUser() ); + $this->assertNotSame( $user, $backend->getUser(), 'sanity check' ); + $backend->setUser( $user ); + $this->assertSame( $user, $backend->getUser() ); + } + + public function testDirty() { + $backend = $this->getBackend(); + $priv = TestingAccessWrapper::newFromObject( $backend ); + $priv->dataDirty = false; + $backend->dirty(); + $this->assertTrue( $priv->dataDirty ); + } + + public function testGetData() { + $backend = $this->getBackend(); + $data = $backend->getData(); + $this->assertSame( [], $data ); + $this->assertTrue( TestingAccessWrapper::newFromObject( $backend )->dataDirty ); + $data['???'] = '!!!'; + $this->assertSame( [ '???' => '!!!' ], $data ); + + $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ]; + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend(); + $this->assertSame( $testData, $backend->getData() ); + $this->assertFalse( TestingAccessWrapper::newFromObject( $backend )->dataDirty ); + } + + public function testAddData() { + $backend = $this->getBackend(); + $priv = TestingAccessWrapper::newFromObject( $backend ); + + $priv->data = [ 'foo' => 1 ]; + $priv->dataDirty = false; + $backend->addData( [ 'foo' => 1 ] ); + $this->assertSame( [ 'foo' => 1 ], $priv->data ); + $this->assertFalse( $priv->dataDirty ); + + $priv->data = [ 'foo' => 1 ]; + $priv->dataDirty = false; + $backend->addData( [ 'foo' => '1' ] ); + $this->assertSame( [ 'foo' => '1' ], $priv->data ); + $this->assertTrue( $priv->dataDirty ); + + $priv->data = [ 'foo' => 1 ]; + $priv->dataDirty = false; + $backend->addData( [ 'bar' => 2 ] ); + $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data ); + $this->assertTrue( $priv->dataDirty ); + } + + public function testDelaySave() { + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $backend = $this->getBackend(); + $priv = TestingAccessWrapper::newFromObject( $backend ); + $priv->persist = true; + + // Saves happen normally when no delay is in effect + $this->onSessionMetadataCalled = false; + $priv->metaDirty = true; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' ); + + $this->onSessionMetadataCalled = false; + $priv->metaDirty = true; + $priv->autosave(); + $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' ); + + $delay = $backend->delaySave(); + + // Autosave doesn't happen when no delay is in effect + $this->onSessionMetadataCalled = false; + $priv->metaDirty = true; + $priv->autosave(); + $this->assertFalse( $this->onSessionMetadataCalled ); + + // Save still does happen when no delay is in effect + $priv->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + + // Save happens when delay is consumed + $this->onSessionMetadataCalled = false; + $priv->metaDirty = true; + \Wikimedia\ScopedCallback::consume( $delay ); + $this->assertTrue( $this->onSessionMetadataCalled ); + + // Test multiple delays + $delay1 = $backend->delaySave(); + $delay2 = $backend->delaySave(); + $delay3 = $backend->delaySave(); + $this->onSessionMetadataCalled = false; + $priv->metaDirty = true; + $priv->autosave(); + $this->assertFalse( $this->onSessionMetadataCalled ); + \Wikimedia\ScopedCallback::consume( $delay3 ); + $this->assertFalse( $this->onSessionMetadataCalled ); + \Wikimedia\ScopedCallback::consume( $delay1 ); + $this->assertFalse( $this->onSessionMetadataCalled ); + \Wikimedia\ScopedCallback::consume( $delay2 ); + $this->assertTrue( $this->onSessionMetadataCalled ); + } + + public function testSave() { + $user = static::getTestSysop()->getUser(); + $this->store = new TestBagOStuff(); + $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ]; + + $neverHook = $this->getMockBuilder( __CLASS__ ) + ->setMethods( [ 'onSessionMetadata' ] )->getMock(); + $neverHook->expects( $this->never() )->method( 'onSessionMetadata' ); + + $builder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistSession', 'unpersistSession' ] ); + + $neverProvider = $builder->getMock(); + $neverProvider->expects( $this->never() )->method( 'persistSession' ); + $neverProvider->expects( $this->never() )->method( 'unpersistSession' ); + + // Not persistent or dirty + $this->provider = $neverProvider; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + + // (but does unpersist if forced) + $this->provider = $builder->getMock(); + $this->provider->expects( $this->never() )->method( 'persistSession' ); + $this->provider->expects( $this->atLeastOnce() )->method( 'unpersistSession' ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = false; + TestingAccessWrapper::newFromObject( $backend )->forcePersist = true; + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + + // (but not to a WebRequest associated with a different session) + $this->provider = $neverProvider; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + TestingAccessWrapper::newFromObject( $backend )->requests[100] + ->setSessionId( new SessionId( 'x' ) ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = false; + TestingAccessWrapper::newFromObject( $backend )->forcePersist = true; + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + + // Not persistent, but dirty + $this->provider = $neverProvider; + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = true; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertFalse( $this->store->getSessionFromBackend( self::SESSIONID ), + 'making sure it didn\'t save to backend' ); + + // Persistent, not dirty + $this->provider = $neverProvider; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + + // (but will persist if forced) + $this->provider = $builder->getMock(); + $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' ); + $this->provider->expects( $this->never() )->method( 'unpersistSession' ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + TestingAccessWrapper::newFromObject( $backend )->forcePersist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + + // Persistent and dirty + $this->provider = $neverProvider; + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = true; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ), + 'making sure it did save to backend' ); + + // (also persists if forced) + $this->provider = $builder->getMock(); + $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' ); + $this->provider->expects( $this->never() )->method( 'unpersistSession' ); + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + TestingAccessWrapper::newFromObject( $backend )->forcePersist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = true; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ), + 'making sure it did save to backend' ); + + // (also persists if metadata dirty) + $this->provider = $builder->getMock(); + $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' ); + $this->provider->expects( $this->never() )->method( 'unpersistSession' ); + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = true; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ), + 'making sure it did save to backend' ); + + // Not marked dirty, but dirty data + // (e.g. indirect modification from ArrayAccess::offsetGet) + $this->provider = $neverProvider; + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = false; + TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match'; + $backend->save(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ), + 'making sure it did save to backend' ); + + // Bad hook + $this->provider = null; + $mockHook = $this->getMockBuilder( __CLASS__ ) + ->setMethods( [ 'onSessionMetadata' ] )->getMock(); + $mockHook->expects( $this->any() )->method( 'onSessionMetadata' ) + ->will( $this->returnCallback( + function ( SessionBackend $backend, array &$metadata, array $requests ) { + $metadata['userId']++; + } + ) ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $backend->dirty(); + try { + $backend->save(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'SessionMetadata hook changed metadata key "userId"', + $ex->getMessage() + ); + } + + // SessionManager::preventSessionsForUser + TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = [ + $user->getName() => true, + ]; + $this->provider = $neverProvider; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + TestingAccessWrapper::newFromObject( $backend )->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + TestingAccessWrapper::newFromObject( $backend )->metaDirty = true; + TestingAccessWrapper::newFromObject( $backend )->dataDirty = true; + $backend->save(); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + } + + public function testRenew() { + $user = static::getTestSysop()->getUser(); + $this->store = new TestBagOStuff(); + $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ]; + + // Not persistent + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistSession' ] )->getMock(); + $this->provider->expects( $this->never() )->method( 'persistSession' ); + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + $wrap->metaDirty = false; + $wrap->dataDirty = false; + $wrap->forcePersist = false; + $wrap->expires = 0; + $backend->renew(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotEquals( 0, $wrap->expires ); + + // Persistent + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistSession' ] )->getMock(); + $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' ); + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $wrap->persist = true; + $this->assertTrue( $backend->isPersistent(), 'sanity check' ); + $wrap->metaDirty = false; + $wrap->dataDirty = false; + $wrap->forcePersist = false; + $wrap->expires = 0; + $backend->renew(); + $this->assertTrue( $this->onSessionMetadataCalled ); + $blob = $this->store->getSession( self::SESSIONID ); + $this->assertInternalType( 'array', $blob ); + $this->assertArrayHasKey( 'metadata', $blob ); + $metadata = $blob['metadata']; + $this->assertInternalType( 'array', $metadata ); + $this->assertArrayHasKey( '???', $metadata ); + $this->assertSame( '!!!', $metadata['???'] ); + $this->assertNotEquals( 0, $wrap->expires ); + + // Not persistent, not expiring + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'persistSession' ] )->getMock(); + $this->provider->expects( $this->never() )->method( 'persistSession' ); + $this->onSessionMetadataCalled = false; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] ); + $this->store->setSessionData( self::SESSIONID, $testData ); + $backend = $this->getBackend( $user ); + $this->store->deleteSession( self::SESSIONID ); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $this->assertFalse( $backend->isPersistent(), 'sanity check' ); + $wrap->metaDirty = false; + $wrap->dataDirty = false; + $wrap->forcePersist = false; + $expires = time() + $wrap->lifetime + 100; + $wrap->expires = $expires; + $backend->renew(); + $this->assertFalse( $this->onSessionMetadataCalled ); + $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' ); + $this->assertEquals( $expires, $wrap->expires ); + } + + public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) { + $this->onSessionMetadataCalled = true; + $metadata['???'] = '!!!'; + } + + public function testTakeOverGlobalSession() { + if ( !PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( SessionManager::singleton() ); + } + if ( !PHPSessionHandler::isEnabled() ) { + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $handler = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) { + session_write_close(); + $handler->enable = false; + } ); + $handler->enable = true; + } + + $backend = $this->getBackend( static::getTestSysop()->getUser() ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true; + + $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager ); + + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $request = \RequestContext::getMain()->getRequest(); + $manager->globalSession = $backend->getSession( $request ); + $manager->globalSessionRequest = $request; + + session_id( '' ); + TestingAccessWrapper::newFromObject( $backend )->checkPHPSession(); + $this->assertSame( $backend->getId(), session_id() ); + session_write_close(); + + $backend2 = $this->getBackend( + User::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + ); + TestingAccessWrapper::newFromObject( $backend2 )->usePhpSessionHandling = true; + + session_id( '' ); + TestingAccessWrapper::newFromObject( $backend2 )->checkPHPSession(); + $this->assertSame( '', session_id() ); + } + + public function testResetIdOfGlobalSession() { + if ( !PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( SessionManager::singleton() ); + } + if ( !PHPSessionHandler::isEnabled() ) { + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $handler = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) { + session_write_close(); + $handler->enable = false; + } ); + $handler->enable = true; + } + + $backend = $this->getBackend( User::newFromName( 'UTSysop' ) ); + TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true; + + $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager ); + + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $request = \RequestContext::getMain()->getRequest(); + $manager->globalSession = $backend->getSession( $request ); + $manager->globalSessionRequest = $request; + + session_id( self::SESSIONID ); + \Wikimedia\quietCall( 'session_start' ); + $_SESSION['foo'] = __METHOD__; + $backend->resetId(); + $this->assertNotEquals( self::SESSIONID, $backend->getId() ); + $this->assertSame( $backend->getId(), session_id() ); + $this->assertArrayHasKey( 'foo', $_SESSION ); + $this->assertSame( __METHOD__, $_SESSION['foo'] ); + session_write_close(); + } + + public function testUnpersistOfGlobalSession() { + if ( !PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( SessionManager::singleton() ); + } + if ( !PHPSessionHandler::isEnabled() ) { + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $handler = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) { + session_write_close(); + $handler->enable = false; + } ); + $handler->enable = true; + } + + $backend = $this->getBackend( User::newFromName( 'UTSysop' ) ); + $wrap = TestingAccessWrapper::newFromObject( $backend ); + $wrap->usePhpSessionHandling = true; + $wrap->persist = true; + + $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager ); + + $manager = TestingAccessWrapper::newFromObject( $this->manager ); + $request = \RequestContext::getMain()->getRequest(); + $manager->globalSession = $backend->getSession( $request ); + $manager->globalSessionRequest = $request; + + session_id( self::SESSIONID . 'x' ); + \Wikimedia\quietCall( 'session_start' ); + $backend->unpersist(); + $this->assertSame( self::SESSIONID . 'x', session_id() ); + session_write_close(); + + session_id( self::SESSIONID ); + $wrap->persist = true; + $backend->unpersist(); + $this->assertSame( '', session_id() ); + } + + public function testGetAllowedUserRights() { + $this->provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'getAllowedUserRights' ] ) + ->getMock(); + $this->provider->expects( $this->any() )->method( 'getAllowedUserRights' ) + ->will( $this->returnValue( [ 'foo', 'bar' ] ) ); + + $backend = $this->getBackend(); + $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionIdTest.php b/www/wiki/tests/phpunit/includes/session/SessionIdTest.php new file mode 100644 index 00000000..2b06d971 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionIdTest.php @@ -0,0 +1,22 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; + +/** + * @group Session + * @covers MediaWiki\Session\SessionId + */ +class SessionIdTest extends MediaWikiTestCase { + + public function testEverything() { + $id = new SessionId( 'foo' ); + $this->assertSame( 'foo', $id->getId() ); + $this->assertSame( 'foo', (string)$id ); + $id->setId( 'bar' ); + $this->assertSame( 'bar', $id->getId() ); + $this->assertSame( 'bar', (string)$id ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionInfoTest.php b/www/wiki/tests/phpunit/includes/session/SessionInfoTest.php new file mode 100644 index 00000000..8f7b2a6e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionInfoTest.php @@ -0,0 +1,356 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\SessionInfo + */ +class SessionInfoTest extends MediaWikiTestCase { + + public function testBasics() { + $anonInfo = UserInfo::newAnonymous(); + $userInfo = UserInfo::newFromName( 'UTSysop', true ); + $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false ); + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY - 1, [] ); + $this->fail( 'Expected exception not thrown', 'priority < min' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' ); + } + + try { + new SessionInfo( SessionInfo::MAX_PRIORITY + 1, [] ); + $this->fail( 'Expected exception not thrown', 'priority > max' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' ); + } + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => 'ABC?' ] ); + $this->fail( 'Expected exception not thrown', 'bad session ID' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' ); + } + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'userInfo' => new \stdClass ] ); + $this->fail( 'Expected exception not thrown', 'bad userInfo' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' ); + } + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY, [] ); + $this->fail( 'Expected exception not thrown', 'no provider, no id' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(), + 'no provider, no id' ); + } + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'copyFrom' => new \stdClass ] ); + $this->fail( 'Expected exception not thrown', 'bad copyFrom' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid copyFrom', $ex->getMessage(), + 'bad copyFrom' ); + } + + $manager = new SessionManager(); + $provider = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] ) + ->getMockForAbstractClass(); + $provider->setManager( $manager ); + $provider->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( true ) ); + $provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( true ) ); + $provider->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Mock' ) ); + + $provider2 = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ 'persistsSessionId', 'canChangeUser', '__toString' ] ) + ->getMockForAbstractClass(); + $provider2->setManager( $manager ); + $provider2->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( true ) ); + $provider2->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( true ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Mock2' ) ); + + try { + new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'userInfo' => $anonInfo, + 'metadata' => 'foo', + ] ); + $this->fail( 'Expected exception not thrown', 'bad metadata' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' ); + } + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'userInfo' => $anonInfo + ] ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertNotNull( $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $anonInfo, $info->getUserInfo() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'userInfo' => $unverifiedUserInfo, + 'metadata' => [ 'Foo' ], + ] ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertNotNull( $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [ 'Foo' ], $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'userInfo' => $userInfo + ] ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertNotNull( $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $userInfo, $info->getUserInfo() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertTrue( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $id = $manager->generateSessionId(); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'persisted' => true, + 'userInfo' => $anonInfo + ] ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertSame( $id, $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $anonInfo, $info->getUserInfo() ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertTrue( $info->wasPersisted() ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertSame( $id, $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $userInfo, $info->getUserInfo() ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertTrue( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'id' => $id, + 'persisted' => true, + 'userInfo' => $userInfo, + 'metadata' => [ 'Foo' ], + ] ); + $this->assertSame( $id, $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertSame( $userInfo, $info->getUserInfo() ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertTrue( $info->wasPersisted() ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'id' => $id, + 'remembered' => true, + 'userInfo' => $userInfo, + ] ); + $this->assertFalse( $info->wasRemembered(), 'no provider' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'remembered' => true, + ] ); + $this->assertFalse( $info->wasRemembered(), 'no user' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'remembered' => true, + 'userInfo' => $anonInfo, + ] ); + $this->assertFalse( $info->wasRemembered(), 'anonymous user' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'remembered' => true, + 'userInfo' => $unverifiedUserInfo, + ] ); + $this->assertFalse( $info->wasRemembered(), 'unverified user' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'remembered' => false, + 'userInfo' => $userInfo, + ] ); + $this->assertFalse( $info->wasRemembered(), 'specific override' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'id' => $id, + 'idIsSafe' => true, + ] ); + $this->assertSame( $id, $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() ); + $this->assertTrue( $info->isIdSafe() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'id' => $id, + 'forceUse' => true, + ] ); + $this->assertFalse( $info->forceUse(), 'no provider' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'forceUse' => true, + ] ); + $this->assertFalse( $info->forceUse(), 'no id' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, [ + 'provider' => $provider, + 'id' => $id, + 'forceUse' => true, + ] ); + $this->assertTrue( $info->forceUse(), 'correct use' ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id, + 'forceHTTPS' => 1, + ] ); + $this->assertTrue( $info->forceHTTPS() ); + + $fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id . 'A', + 'provider' => $provider, + 'userInfo' => $userInfo, + 'idIsSafe' => true, + 'forceUse' => true, + 'persisted' => true, + 'remembered' => true, + 'forceHTTPS' => true, + 'metadata' => [ 'foo!' ], + ] ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [ + 'copyFrom' => $fromInfo, + ] ); + $this->assertSame( $id . 'A', $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() ); + $this->assertSame( $provider, $info->getProvider() ); + $this->assertSame( $userInfo, $info->getUserInfo() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertTrue( $info->forceUse() ); + $this->assertTrue( $info->wasPersisted() ); + $this->assertTrue( $info->wasRemembered() ); + $this->assertTrue( $info->forceHTTPS() ); + $this->assertSame( [ 'foo!' ], $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, [ + 'id' => $id . 'X', + 'provider' => $provider2, + 'userInfo' => $unverifiedUserInfo, + 'idIsSafe' => false, + 'forceUse' => false, + 'persisted' => false, + 'remembered' => false, + 'forceHTTPS' => false, + 'metadata' => null, + 'copyFrom' => $fromInfo, + ] ); + $this->assertSame( $id . 'X', $info->getId() ); + $this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() ); + $this->assertSame( $provider2, $info->getProvider() ); + $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertFalse( $info->forceUse() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertNull( $info->getProviderMetadata() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id, + ] ); + $this->assertSame( + '[' . SessionInfo::MIN_PRIORITY . "]null<null>$id", + (string)$info, + 'toString' + ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'persisted' => true, + 'userInfo' => $userInfo + ] ); + $this->assertSame( + '[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id", + (string)$info, + 'toString' + ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'persisted' => true, + 'userInfo' => $unverifiedUserInfo + ] ); + $this->assertSame( + '[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id", + (string)$info, + 'toString' + ); + } + + public function testCompare() { + $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ 'id' => $id ] ); + $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ 'id' => $id ] ); + + $this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' ); + $this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' ); + $this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' ); + } +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php b/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php new file mode 100644 index 00000000..b33cd24a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionManagerTest.php @@ -0,0 +1,1521 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use Psr\Log\LogLevel; +use User; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\SessionManager + */ +class SessionManagerTest extends MediaWikiTestCase { + + /** @var \HashConfig */ + private $config; + + /** @var \TestLogger */ + private $logger; + + /** @var TestBagOStuff */ + private $store; + + protected function getManager() { + \ObjectCache::$instances['testSessionStore'] = new TestBagOStuff(); + $this->config = new \HashConfig( [ + 'LanguageCode' => 'en', + 'SessionCacheType' => 'testSessionStore', + 'ObjectCacheSessionExpiry' => 100, + 'SessionProviders' => [ + [ 'class' => \DummySessionProvider::class ], + ] + ] ); + $this->logger = new \TestLogger( false, function ( $m ) { + return substr( $m, 0, 15 ) === 'SessionBackend ' ? null : $m; + } ); + $this->store = new TestBagOStuff(); + + return new SessionManager( [ + 'config' => $this->config, + 'logger' => $this->logger, + 'store' => $this->store, + ] ); + } + + protected function objectCacheDef( $object ) { + return [ 'factory' => function () use ( $object ) { + return $object; + } ]; + } + + public function testSingleton() { + $reset = TestUtils::setSessionManagerSingleton( null ); + + $singleton = SessionManager::singleton(); + $this->assertInstanceOf( SessionManager::class, $singleton ); + $this->assertSame( $singleton, SessionManager::singleton() ); + } + + public function testGetGlobalSession() { + $context = \RequestContext::getMain(); + + if ( !PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( SessionManager::singleton() ); + } + $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' ); + $rProp->setAccessible( true ); + $handler = TestingAccessWrapper::newFromObject( $rProp->getValue() ); + $oldEnable = $handler->enable; + $reset[] = new \Wikimedia\ScopedCallback( function () use ( $handler, $oldEnable ) { + if ( $handler->enable ) { + session_write_close(); + } + $handler->enable = $oldEnable; + } ); + $reset[] = TestUtils::setSessionManagerSingleton( $this->getManager() ); + + $handler->enable = true; + $request = new \FauxRequest(); + $context->setRequest( $request ); + $id = $request->getSession()->getId(); + + session_write_close(); + session_id( '' ); + $session = SessionManager::getGlobalSession(); + $this->assertSame( $id, $session->getId() ); + + session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); + $session = SessionManager::getGlobalSession(); + $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() ); + $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() ); + + session_write_close(); + $handler->enable = false; + $request = new \FauxRequest(); + $context->setRequest( $request ); + $id = $request->getSession()->getId(); + + session_id( '' ); + $session = SessionManager::getGlobalSession(); + $this->assertSame( $id, $session->getId() ); + + session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); + $session = SessionManager::getGlobalSession(); + $this->assertSame( $id, $session->getId() ); + $this->assertSame( $id, $request->getSession()->getId() ); + } + + public function testConstructor() { + $manager = TestingAccessWrapper::newFromObject( $this->getManager() ); + $this->assertSame( $this->config, $manager->config ); + $this->assertSame( $this->logger, $manager->logger ); + $this->assertSame( $this->store, $manager->store ); + + $manager = TestingAccessWrapper::newFromObject( new SessionManager() ); + $this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config ); + + $manager = TestingAccessWrapper::newFromObject( new SessionManager( [ + 'config' => $this->config, + ] ) ); + $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store ); + + foreach ( [ + 'config' => '$options[\'config\'] must be an instance of Config', + 'logger' => '$options[\'logger\'] must be an instance of LoggerInterface', + 'store' => '$options[\'store\'] must be an instance of BagOStuff', + ] as $key => $error ) { + try { + new SessionManager( [ $key => new \stdClass ] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( $error, $ex->getMessage() ); + } + } + } + + public function testGetSessionForRequest() { + $manager = $this->getManager(); + $request = new \FauxRequest(); + $request->unpersist1 = false; + $request->unpersist2 = false; + + $id1 = ''; + $id2 = ''; + $idEmpty = 'empty-session-------------------'; + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( + [ 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe', 'unpersistSession' ] + ); + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->any() )->method( 'provideSessionInfo' ) + ->with( $this->identicalTo( $request ) ) + ->will( $this->returnCallback( function ( $request ) { + return $request->info1; + } ) ); + $provider1->expects( $this->any() )->method( 'newSessionInfo' ) + ->will( $this->returnCallback( function () use ( $idEmpty, $provider1 ) { + return new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => $idEmpty, + 'persisted' => true, + 'idIsSafe' => true, + ] ); + } ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Provider1' ) ); + $provider1->expects( $this->any() )->method( 'describe' ) + ->will( $this->returnValue( '#1 sessions' ) ); + $provider1->expects( $this->any() )->method( 'unpersistSession' ) + ->will( $this->returnCallback( function ( $request ) { + $request->unpersist1 = true; + } ) ); + + $provider2 = $providerBuilder->getMock(); + $provider2->expects( $this->any() )->method( 'provideSessionInfo' ) + ->with( $this->identicalTo( $request ) ) + ->will( $this->returnCallback( function ( $request ) { + return $request->info2; + } ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Provider2' ) ); + $provider2->expects( $this->any() )->method( 'describe' ) + ->will( $this->returnValue( '#2 sessions' ) ); + $provider2->expects( $this->any() )->method( 'unpersistSession' ) + ->will( $this->returnCallback( function ( $request ) { + $request->unpersist2 = true; + } ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + $this->objectCacheDef( $provider2 ), + ] ); + + // No provider returns info + $request->info1 = null; + $request->info2 = null; + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $idEmpty, $session->getId() ); + $this->assertFalse( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + + // Both providers return info, picks best one + $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ + 'provider' => $provider2, + 'id' => ( $id2 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id2, $session->getId() ); + $this->assertFalse( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + + $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ + 'provider' => $provider2, + 'id' => ( $id2 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id1, $session->getId() ); + $this->assertFalse( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + + // Tied priorities + $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'userInfo' => UserInfo::newAnonymous(), + 'idIsSafe' => true, + ] ); + $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider2, + 'id' => ( $id2 = $manager->generateSessionId() ), + 'persisted' => true, + 'userInfo' => UserInfo::newAnonymous(), + 'idIsSafe' => true, + ] ); + try { + $manager->getSessionForRequest( $request ); + $this->fail( 'Expcected exception not thrown' ); + } catch ( \OverflowException $ex ) { + $this->assertStringStartsWith( + 'Multiple sessions for this request tied for top priority: ', + $ex->getMessage() + ); + $this->assertCount( 2, $ex->sessionInfos ); + $this->assertContains( $request->info1, $ex->sessionInfos ); + $this->assertContains( $request->info2, $ex->sessionInfos ); + } + $this->assertFalse( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + + // Bad provider + $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider2, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $request->info2 = null; + try { + $manager->getSessionForRequest( $request ); + $this->fail( 'Expcected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'Provider1 returned session info for a different provider: ' . $request->info1, + $ex->getMessage() + ); + } + $this->assertFalse( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + + // Unusable session info + $this->logger->setCollect( true ); + $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', false ), + 'idIsSafe' => true, + ] ); + $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => ( $id2 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id2, $session->getId() ); + $this->logger->setCollect( false ); + $this->assertTrue( $request->unpersist1 ); + $this->assertFalse( $request->unpersist2 ); + $request->unpersist1 = false; + + $this->logger->setCollect( true ); + $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider2, + 'id' => ( $id2 = $manager->generateSessionId() ), + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', false ), + 'idIsSafe' => true, + ] ); + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id1, $session->getId() ); + $this->logger->setCollect( false ); + $this->assertFalse( $request->unpersist1 ); + $this->assertTrue( $request->unpersist2 ); + $request->unpersist2 = false; + + // Unpersisted session ID + $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [ + 'provider' => $provider1, + 'id' => ( $id1 = $manager->generateSessionId() ), + 'persisted' => false, + 'userInfo' => UserInfo::newFromName( 'UTSysop', true ), + 'idIsSafe' => true, + ] ); + $request->info2 = null; + $session = $manager->getSessionForRequest( $request ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id1, $session->getId() ); + $this->assertTrue( $request->unpersist1 ); // The saving of the session does it + $this->assertFalse( $request->unpersist2 ); + $session->persist(); + $this->assertTrue( $session->isPersistent(), 'sanity check' ); + } + + public function testGetSessionById() { + $manager = $this->getManager(); + try { + $manager->getSessionById( 'bad' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid session ID', $ex->getMessage() ); + } + + // Unknown session ID + $id = $manager->generateSessionId(); + $session = $manager->getSessionById( $id, true ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id, $session->getId() ); + + $id = $manager->generateSessionId(); + $this->assertNull( $manager->getSessionById( $id, false ) ); + + // Known but unloadable session ID + $this->logger->setCollect( true ); + $id = $manager->generateSessionId(); + $this->store->setSession( $id, [ 'metadata' => [ + 'userId' => User::idFromName( 'UTSysop' ), + 'userToken' => 'bad', + ] ] ); + + $this->assertNull( $manager->getSessionById( $id, true ) ); + $this->assertNull( $manager->getSessionById( $id, false ) ); + $this->logger->setCollect( false ); + + // Known session ID + $this->store->setSession( $id, [] ); + $session = $manager->getSessionById( $id, false ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $id, $session->getId() ); + + // Store isn't checked if the session is already loaded + $this->store->setSession( $id, [ 'metadata' => [ + 'userId' => User::idFromName( 'UTSysop' ), + 'userToken' => 'bad', + ] ] ); + $session2 = $manager->getSessionById( $id, false ); + $this->assertInstanceOf( Session::class, $session2 ); + $this->assertSame( $id, $session2->getId() ); + unset( $session, $session2 ); + $this->logger->setCollect( true ); + $this->assertNull( $manager->getSessionById( $id, true ) ); + $this->logger->setCollect( false ); + + // Failure to create an empty session + $manager = $this->getManager(); + $provider = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] ) + ->getMock(); + $provider->expects( $this->any() )->method( 'provideSessionInfo' ) + ->will( $this->returnValue( null ) ); + $provider->expects( $this->any() )->method( 'newSessionInfo' ) + ->will( $this->returnValue( null ) ); + $provider->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider' ) ); + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider ), + ] ); + $this->logger->setCollect( true ); + $this->assertNull( $manager->getSessionById( $id, true ) ); + $this->logger->setCollect( false ); + $this->assertSame( [ + [ LogLevel::ERROR, 'Failed to create empty session: {exception}' ] + ], $this->logger->getBuffer() ); + } + + public function testGetEmptySession() { + $manager = $this->getManager(); + $pmanager = TestingAccessWrapper::newFromObject( $manager ); + $request = new \FauxRequest(); + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] ); + + $expectId = null; + $info1 = null; + $info2 = null; + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->any() )->method( 'provideSessionInfo' ) + ->will( $this->returnValue( null ) ); + $provider1->expects( $this->any() )->method( 'newSessionInfo' ) + ->with( $this->callback( function ( $id ) use ( &$expectId ) { + return $id === $expectId; + } ) ) + ->will( $this->returnCallback( function () use ( &$info1 ) { + return $info1; + } ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider1' ) ); + + $provider2 = $providerBuilder->getMock(); + $provider2->expects( $this->any() )->method( 'provideSessionInfo' ) + ->will( $this->returnValue( null ) ); + $provider2->expects( $this->any() )->method( 'newSessionInfo' ) + ->with( $this->callback( function ( $id ) use ( &$expectId ) { + return $id === $expectId; + } ) ) + ->will( $this->returnCallback( function () use ( &$info2 ) { + return $info2; + } ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider2' ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + $this->objectCacheDef( $provider2 ), + ] ); + + // No info + $expectId = null; + $info1 = null; + $info2 = null; + try { + $manager->getEmptySession(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'No provider could provide an empty session!', + $ex->getMessage() + ); + } + + // Info + $expectId = null; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => 'empty---------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = null; + $session = $manager->getEmptySession(); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( 'empty---------------------------', $session->getId() ); + + // Info, explicitly + $expectId = 'expected------------------------'; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => $expectId, + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = null; + $session = $pmanager->getEmptySessionInternal( null, $expectId ); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( $expectId, $session->getId() ); + + // Wrong ID + $expectId = 'expected-----------------------2'; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => "un$expectId", + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = null; + try { + $pmanager->getEmptySessionInternal( null, $expectId ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'MockProvider1 returned empty session info with a wrong id: ' . + "un$expectId != $expectId", + $ex->getMessage() + ); + } + + // Unsafe ID + $expectId = 'expected-----------------------2'; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => $expectId, + 'persisted' => true, + ] ); + $info2 = null; + try { + $pmanager->getEmptySessionInternal( null, $expectId ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'MockProvider1 returned empty session info with id flagged unsafe', + $ex->getMessage() + ); + } + + // Wrong provider + $expectId = null; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => 'empty---------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = null; + try { + $manager->getEmptySession(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'MockProvider1 returned an empty session info for a different provider: ' . $info1, + $ex->getMessage() + ); + } + + // Highest priority wins + $expectId = null; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ + 'provider' => $provider1, + 'id' => 'empty1--------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => 'empty2--------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $session = $manager->getEmptySession(); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( 'empty1--------------------------', $session->getId() ); + + $expectId = null; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [ + 'provider' => $provider1, + 'id' => 'empty1--------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [ + 'provider' => $provider2, + 'id' => 'empty2--------------------------', + 'persisted' => true, + 'idIsSafe' => true, + ] ); + $session = $manager->getEmptySession(); + $this->assertInstanceOf( Session::class, $session ); + $this->assertSame( 'empty2--------------------------', $session->getId() ); + + // Tied priorities throw an exception + $expectId = null; + $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider1, + 'id' => 'empty1--------------------------', + 'persisted' => true, + 'userInfo' => UserInfo::newAnonymous(), + 'idIsSafe' => true, + ] ); + $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => 'empty2--------------------------', + 'persisted' => true, + 'userInfo' => UserInfo::newAnonymous(), + 'idIsSafe' => true, + ] ); + try { + $manager->getEmptySession(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertStringStartsWith( + 'Multiple empty sessions tied for top priority: ', + $ex->getMessage() + ); + } + + // Bad id + try { + $pmanager->getEmptySessionInternal( null, 'bad' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid session ID', $ex->getMessage() ); + } + + // Session already exists + $expectId = 'expected-----------------------3'; + $this->store->setSessionMeta( $expectId, [ + 'provider' => 'MockProvider2', + 'userId' => 0, + 'userName' => null, + 'userToken' => null, + ] ); + try { + $pmanager->getEmptySessionInternal( null, $expectId ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Session ID already exists', $ex->getMessage() ); + } + } + + public function testInvalidateSessionsForUser() { + $user = User::newFromName( 'UTSysop' ); + $manager = $this->getManager(); + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'invalidateSessionsForUser', '__toString' ] ); + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->once() )->method( 'invalidateSessionsForUser' ) + ->with( $this->identicalTo( $user ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider1' ) ); + + $provider2 = $providerBuilder->getMock(); + $provider2->expects( $this->once() )->method( 'invalidateSessionsForUser' ) + ->with( $this->identicalTo( $user ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider2' ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + $this->objectCacheDef( $provider2 ), + ] ); + + $oldToken = $user->getToken( true ); + $manager->invalidateSessionsForUser( $user ); + $this->assertNotEquals( $oldToken, $user->getToken() ); + } + + public function testGetVaryHeaders() { + $manager = $this->getManager(); + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'getVaryHeaders', '__toString' ] ); + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->once() )->method( 'getVaryHeaders' ) + ->will( $this->returnValue( [ + 'Foo' => null, + 'Bar' => [ 'X', 'Bar1' ], + 'Quux' => null, + ] ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider1' ) ); + + $provider2 = $providerBuilder->getMock(); + $provider2->expects( $this->once() )->method( 'getVaryHeaders' ) + ->will( $this->returnValue( [ + 'Baz' => null, + 'Bar' => [ 'X', 'Bar2' ], + 'Quux' => [ 'Quux' ], + ] ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider2' ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + $this->objectCacheDef( $provider2 ), + ] ); + + $expect = [ + 'Foo' => [], + 'Bar' => [ 'X', 'Bar1', 3 => 'Bar2' ], + 'Quux' => [ 'Quux' ], + 'Baz' => [], + ]; + + $this->assertEquals( $expect, $manager->getVaryHeaders() ); + + // Again, to ensure it's cached + $this->assertEquals( $expect, $manager->getVaryHeaders() ); + } + + public function testGetVaryCookies() { + $manager = $this->getManager(); + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'getVaryCookies', '__toString' ] ); + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->once() )->method( 'getVaryCookies' ) + ->will( $this->returnValue( [ 'Foo', 'Bar' ] ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider1' ) ); + + $provider2 = $providerBuilder->getMock(); + $provider2->expects( $this->once() )->method( 'getVaryCookies' ) + ->will( $this->returnValue( [ 'Foo', 'Baz' ] ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider2' ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + $this->objectCacheDef( $provider2 ), + ] ); + + $expect = [ 'Foo', 'Bar', 'Baz' ]; + + $this->assertEquals( $expect, $manager->getVaryCookies() ); + + // Again, to ensure it's cached + $this->assertEquals( $expect, $manager->getVaryCookies() ); + } + + public function testGetProviders() { + $realManager = $this->getManager(); + $manager = TestingAccessWrapper::newFromObject( $realManager ); + + $this->config->set( 'SessionProviders', [ + [ 'class' => \DummySessionProvider::class ], + ] ); + $providers = $manager->getProviders(); + $this->assertArrayHasKey( 'DummySessionProvider', $providers ); + $provider = TestingAccessWrapper::newFromObject( $providers['DummySessionProvider'] ); + $this->assertSame( $manager->logger, $provider->logger ); + $this->assertSame( $manager->config, $provider->config ); + $this->assertSame( $realManager, $provider->getManager() ); + + $this->config->set( 'SessionProviders', [ + [ 'class' => \DummySessionProvider::class ], + [ 'class' => \DummySessionProvider::class ], + ] ); + $manager->sessionProviders = null; + try { + $manager->getProviders(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \UnexpectedValueException $ex ) { + $this->assertSame( + 'Duplicate provider name "DummySessionProvider"', + $ex->getMessage() + ); + } + } + + public function testShutdown() { + $manager = TestingAccessWrapper::newFromObject( $this->getManager() ); + $manager->setLogger( new \Psr\Log\NullLogger() ); + + $mock = $this->getMockBuilder( stdClass::class ) + ->setMethods( [ 'shutdown' ] )->getMock(); + $mock->expects( $this->once() )->method( 'shutdown' ); + + $manager->allSessionBackends = [ $mock ]; + $manager->shutdown(); + } + + public function testGetSessionFromInfo() { + $manager = TestingAccessWrapper::newFromObject( $this->getManager() ); + $request = new \FauxRequest(); + + $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $manager->getProvider( 'DummySessionProvider' ), + 'id' => $id, + 'persisted' => true, + 'userInfo' => UserInfo::newFromName( 'UTSysop', true ), + 'idIsSafe' => true, + ] ); + TestingAccessWrapper::newFromObject( $info )->idIsSafe = true; + $session1 = TestingAccessWrapper::newFromObject( + $manager->getSessionFromInfo( $info, $request ) + ); + $session2 = TestingAccessWrapper::newFromObject( + $manager->getSessionFromInfo( $info, $request ) + ); + + $this->assertSame( $session1->backend, $session2->backend ); + $this->assertNotEquals( $session1->index, $session2->index ); + $this->assertSame( $session1->getSessionId(), $session2->getSessionId() ); + $this->assertSame( $id, $session1->getId() ); + + TestingAccessWrapper::newFromObject( $info )->idIsSafe = false; + $session3 = $manager->getSessionFromInfo( $info, $request ); + $this->assertNotSame( $id, $session3->getId() ); + } + + public function testBackendRegistration() { + $manager = $this->getManager(); + + $session = $manager->getSessionForRequest( new \FauxRequest ); + $backend = TestingAccessWrapper::newFromObject( $session )->backend; + $sessionId = $session->getSessionId(); + $id = (string)$sessionId; + + $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() ); + + $manager->changeBackendId( $backend ); + $this->assertSame( $sessionId, $session->getSessionId() ); + $this->assertNotEquals( $id, (string)$sessionId ); + $id = (string)$sessionId; + + $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() ); + + // Destruction of the session here causes the backend to be deregistered + $session = null; + + try { + $manager->changeBackendId( $backend ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'Backend was not registered with this SessionManager', $ex->getMessage() + ); + } + + try { + $manager->deregisterSessionBackend( $backend ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'Backend was not registered with this SessionManager', $ex->getMessage() + ); + } + + $session = $manager->getSessionById( $id, true ); + $this->assertSame( $sessionId, $session->getSessionId() ); + } + + public function testGenerateSessionId() { + $manager = $this->getManager(); + + $id = $manager->generateSessionId(); + $this->assertTrue( SessionManager::validateSessionId( $id ), "Generated ID: $id" ); + } + + public function testPreventSessionsForUser() { + $manager = $this->getManager(); + + $providerBuilder = $this->getMockBuilder( \DummySessionProvider::class ) + ->setMethods( [ 'preventSessionsForUser', '__toString' ] ); + + $provider1 = $providerBuilder->getMock(); + $provider1->expects( $this->once() )->method( 'preventSessionsForUser' ) + ->with( $this->equalTo( 'UTSysop' ) ); + $provider1->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'MockProvider1' ) ); + + $this->config->set( 'SessionProviders', [ + $this->objectCacheDef( $provider1 ), + ] ); + + $this->assertFalse( $manager->isUserSessionPrevented( 'UTSysop' ) ); + $manager->preventSessionsForUser( 'UTSysop' ); + $this->assertTrue( $manager->isUserSessionPrevented( 'UTSysop' ) ); + } + + public function testLoadSessionInfoFromStore() { + $manager = $this->getManager(); + $logger = new \TestLogger( true ); + $manager->setLogger( $logger ); + $request = new \FauxRequest(); + + // TestingAccessWrapper can't handle methods with reference arguments, sigh. + $rClass = new \ReflectionClass( $manager ); + $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' ); + $rMethod->setAccessible( true ); + $loadSessionInfoFromStore = function ( &$info ) use ( $rMethod, $manager, $request ) { + return $rMethod->invokeArgs( $manager, [ &$info, $request ] ); + }; + + $userInfo = UserInfo::newFromName( 'UTSysop', true ); + $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false ); + + $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $metadata = [ + 'userId' => $userInfo->getId(), + 'userName' => $userInfo->getName(), + 'userToken' => $userInfo->getToken( true ), + 'provider' => 'Mock', + ]; + + $builder = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ '__toString', 'mergeMetadata', 'refreshSessionInfo' ] ); + + $provider = $builder->getMockForAbstractClass(); + $provider->setManager( $manager ); + $provider->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( true ) ); + $provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( true ) ); + $provider->expects( $this->any() )->method( 'refreshSessionInfo' ) + ->will( $this->returnValue( true ) ); + $provider->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Mock' ) ); + $provider->expects( $this->any() )->method( 'mergeMetadata' ) + ->will( $this->returnCallback( function ( $a, $b ) { + if ( $b === [ 'Throw' ] ) { + throw new MetadataMergeException( 'no merge!' ); + } + return [ 'Merged' ]; + } ) ); + + $provider2 = $builder->getMockForAbstractClass(); + $provider2->setManager( $manager ); + $provider2->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( false ) ); + $provider2->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( false ) ); + $provider2->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Mock2' ) ); + $provider2->expects( $this->any() )->method( 'refreshSessionInfo' ) + ->will( $this->returnCallback( function ( $info, $request, &$metadata ) { + $metadata['changed'] = true; + return true; + } ) ); + + $provider3 = $builder->getMockForAbstractClass(); + $provider3->setManager( $manager ); + $provider3->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( true ) ); + $provider3->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( true ) ); + $provider3->expects( $this->once() )->method( 'refreshSessionInfo' ) + ->will( $this->returnValue( false ) ); + $provider3->expects( $this->any() )->method( '__toString' ) + ->will( $this->returnValue( 'Mock3' ) ); + + TestingAccessWrapper::newFromObject( $manager )->sessionProviders = [ + (string)$provider => $provider, + (string)$provider2 => $provider2, + (string)$provider3 => $provider3, + ]; + + // No metadata, basic usage + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Unverified user, no metadata + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $unverifiedUserInfo + ] ); + $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ + LogLevel::INFO, + 'Session "{session}": Unverified user provided and no metadata to auth it', + ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // No metadata, missing data + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Null provider and no metadata' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertInstanceOf( UserInfo::class, $info->getUserInfo() ); + $this->assertTrue( $info->getUserInfo()->isVerified() ); + $this->assertTrue( $info->getUserInfo()->isAnon() ); + $this->assertFalse( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => $id, + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::INFO, 'Session "{session}": No user provided and provider cannot set user' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Incomplete/bad metadata + $this->store->setRawSession( $id, true ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad data' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $this->store->setRawSession( $id, [ 'data' => [] ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $this->store->deleteSession( $id ); + $this->store->setRawSession( $id, [ 'metadata' => $metadata ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => true ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $this->store->setRawSession( $id, [ 'metadata' => true, 'data' => [] ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + foreach ( $metadata as $key => $dummy ) { + $tmp = $metadata; + unset( $tmp[$key] ); + $this->store->setRawSession( $id, [ 'metadata' => $tmp, 'data' => [] ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Bad metadata' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + } + + // Basic usage with metadata + $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => [] ] ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Mismatched provider + $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Wrong provider Bad !== Mock' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Unknown provider + $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Unknown provider Bad' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Fill in provider + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Bad user metadata + $this->store->setSessionMeta( $id, [ 'userId' => -1, 'userToken' => null ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::ERROR, 'Session "{session}": {exception}' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $this->store->setSessionMeta( + $id, [ 'userId' => 0, 'userName' => '<X>', 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::ERROR, 'Session "{session}": {exception}', ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Mismatched user by ID + $this->store->setSessionMeta( + $id, [ 'userId' => $userInfo->getId() + 1, 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Mismatched user by name + $this->store->setSessionMeta( + $id, [ 'userId' => 0, 'userName' => 'X', 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // ID matches, name doesn't + $this->store->setSessionMeta( + $id, [ 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ + LogLevel::WARNING, + 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}' + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Mismatched anon user + $this->store->setSessionMeta( + $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ + LogLevel::WARNING, + 'Session "{session}": Metadata has an anonymous user, ' . + 'but a non-anon user was provided', + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Lookup user by ID + $this->store->setSessionMeta( $id, [ 'userToken' => null ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Lookup user by name + $this->store->setSessionMeta( + $id, [ 'userId' => 0, 'userName' => 'UTSysop', 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Lookup anonymous user + $this->store->setSessionMeta( + $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata + ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->getUserInfo()->isAnon() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Unverified user with metadata + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $unverifiedUserInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->getUserInfo()->isVerified() ); + $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() ); + $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Unverified user with metadata + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $unverifiedUserInfo + ] ); + $this->assertFalse( $info->isIdSafe(), 'sanity check' ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->getUserInfo()->isVerified() ); + $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() ); + $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() ); + $this->assertTrue( $info->isIdSafe() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Wrong token + $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Provider metadata + $this->store->setSessionMeta( $id, [ 'provider' => 'Mock2' ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider2, + 'id' => $id, + 'userInfo' => $userInfo, + 'metadata' => [ 'Info' ], + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ 'Info', 'changed' => true ], $info->getProviderMetadata() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'providerMetadata' => [ 'Saved' ] ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ 'Saved' ], $info->getProviderMetadata() ); + $this->assertSame( [], $logger->getBuffer() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + 'metadata' => [ 'Info' ], + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ 'Merged' ], $info->getProviderMetadata() ); + $this->assertSame( [], $logger->getBuffer() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + 'metadata' => [ 'Throw' ], + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [ + [ + LogLevel::WARNING, + 'Session "{session}": Metadata merge failed: {exception}', + ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Remember from session + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertFalse( $info->wasRemembered() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'remember' => true ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->wasRemembered() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'remember' => false ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->wasRemembered() ); + $this->assertSame( [], $logger->getBuffer() ); + + // forceHTTPS from session + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertFalse( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'forceHTTPS' => true ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'forceHTTPS' => false ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + 'forceHTTPS' => true + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->forceHTTPS() ); + $this->assertSame( [], $logger->getBuffer() ); + + // "Persist" flag from session + $this->store->setSessionMeta( $id, $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'persisted' => true ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->wasPersisted() ); + $this->assertSame( [], $logger->getBuffer() ); + + $this->store->setSessionMeta( $id, [ 'persisted' => false ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + 'persisted' => true + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $info->wasPersisted() ); + $this->assertSame( [], $logger->getBuffer() ); + + // Provider refreshSessionInfo() returning false + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider3, + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertSame( [], $logger->getBuffer() ); + + // Hook + $called = false; + $data = [ 'foo' => 1 ]; + $this->store->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo + ] ); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'SessionCheckInfo' => [ function ( &$reason, $i, $r, $m, $d ) use ( + $info, $metadata, $data, $request, &$called + ) { + $this->assertSame( $info->getId(), $i->getId() ); + $this->assertSame( $info->getProvider(), $i->getProvider() ); + $this->assertSame( $info->getUserInfo(), $i->getUserInfo() ); + $this->assertSame( $request, $r ); + $this->assertEquals( $metadata, $m ); + $this->assertEquals( $data, $d ); + $called = true; + return false; + } ] + ] ); + $this->assertFalse( $loadSessionInfoFromStore( $info ) ); + $this->assertTrue( $called ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": Hook aborted' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionCheckInfo' => [] ] ); + + // forceUse deletes bad backend data + $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata ); + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'provider' => $provider, + 'id' => $id, + 'userInfo' => $userInfo, + 'forceUse' => true, + ] ); + $this->assertTrue( $loadSessionInfoFromStore( $info ) ); + $this->assertFalse( $this->store->getSession( $id ) ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ], + ], $logger->getBuffer() ); + $logger->clearBuffer(); + } +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionProviderTest.php b/www/wiki/tests/phpunit/includes/session/SessionProviderTest.php new file mode 100644 index 00000000..052c0167 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionProviderTest.php @@ -0,0 +1,206 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\SessionProvider + */ +class SessionProviderTest extends MediaWikiTestCase { + + public function testBasics() { + $manager = new SessionManager(); + $logger = new \TestLogger(); + $config = new \HashConfig(); + + $provider = $this->getMockForAbstractClass( SessionProvider::class ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + + $provider->setConfig( $config ); + $this->assertSame( $config, $priv->config ); + $provider->setLogger( $logger ); + $this->assertSame( $logger, $priv->logger ); + $provider->setManager( $manager ); + $this->assertSame( $manager, $priv->manager ); + $this->assertSame( $manager, $provider->getManager() ); + + $provider->invalidateSessionsForUser( new \User ); + + $this->assertSame( [], $provider->getVaryHeaders() ); + $this->assertSame( [], $provider->getVaryCookies() ); + $this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) ); + + $this->assertSame( get_class( $provider ), (string)$provider ); + + $this->assertNull( $provider->getRememberUserDuration() ); + + $this->assertNull( $provider->whyNoSession() ); + + $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ + 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'provider' => $provider, + ] ); + $metadata = [ 'foo' ]; + $this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) ); + $this->assertSame( [ 'foo' ], $metadata ); + } + + /** + * @dataProvider provideNewSessionInfo + * @param bool $persistId Return value for ->persistsSessionId() + * @param bool $persistUser Return value for ->persistsSessionUser() + * @param bool $ok Whether a SessionInfo is provided + */ + public function testNewSessionInfo( $persistId, $persistUser, $ok ) { + $manager = new SessionManager(); + + $provider = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] ) + ->getMockForAbstractClass(); + $provider->expects( $this->any() )->method( 'persistsSessionId' ) + ->will( $this->returnValue( $persistId ) ); + $provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( $persistUser ) ); + $provider->setManager( $manager ); + + if ( $ok ) { + $info = $provider->newSessionInfo(); + $this->assertNotNull( $info ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertTrue( $info->isIdSafe() ); + + $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + $info = $provider->newSessionInfo( $id ); + $this->assertNotNull( $info ); + $this->assertSame( $id, $info->getId() ); + $this->assertFalse( $info->wasPersisted() ); + $this->assertTrue( $info->isIdSafe() ); + } else { + $this->assertNull( $provider->newSessionInfo() ); + } + } + + public function testMergeMetadata() { + $provider = $this->getMockBuilder( SessionProvider::class ) + ->getMockForAbstractClass(); + + try { + $provider->mergeMetadata( + [ 'foo' => 1, 'baz' => 3 ], + [ 'bar' => 2, 'baz' => '3' ] + ); + $this->fail( 'Expected exception not thrown' ); + } catch ( MetadataMergeException $ex ) { + $this->assertSame( 'Key "baz" changed', $ex->getMessage() ); + $this->assertSame( + [ 'old_value' => 3, 'new_value' => '3' ], $ex->getContext() ); + } + + $res = $provider->mergeMetadata( + [ 'foo' => 1, 'baz' => 3 ], + [ 'bar' => 2, 'baz' => 3 ] + ); + $this->assertSame( [ 'bar' => 2, 'baz' => 3 ], $res ); + } + + public static function provideNewSessionInfo() { + return [ + [ false, false, false ], + [ true, false, false ], + [ false, true, false ], + [ true, true, true ], + ]; + } + + public function testImmutableSessions() { + $provider = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] ) + ->getMockForAbstractClass(); + $provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( true ) ); + $provider->preventSessionsForUser( 'Foo' ); + + $provider = $this->getMockBuilder( SessionProvider::class ) + ->setMethods( [ 'canChangeUser', 'persistsSessionId' ] ) + ->getMockForAbstractClass(); + $provider->expects( $this->any() )->method( 'canChangeUser' ) + ->will( $this->returnValue( false ) ); + try { + $provider->preventSessionsForUser( 'Foo' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \BadMethodCallException $ex ) { + $this->assertSame( + 'MediaWiki\\Session\\SessionProvider::preventSessionsForUser must be implmented ' . + 'when canChangeUser() is false', + $ex->getMessage() + ); + } + } + + public function testHashToSessionId() { + $config = new \HashConfig( [ + 'SecretKey' => 'Shhh!', + ] ); + + $provider = $this->getMockForAbstractClass( SessionProvider::class, + [], 'MockSessionProvider' ); + $provider->setConfig( $config ); + $priv = TestingAccessWrapper::newFromObject( $provider ); + + $this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) ); + $this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9', + $priv->hashToSessionId( 'foobar', 'secret' ) ); + + try { + $priv->hashToSessionId( [] ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + '$data must be a string, array was passed', + $ex->getMessage() + ); + } + try { + $priv->hashToSessionId( '', false ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + '$key must be a string or null, boolean was passed', + $ex->getMessage() + ); + } + } + + public function testDescribe() { + $provider = $this->getMockForAbstractClass( SessionProvider::class, + [], 'MockSessionProvider' ); + + $this->assertSame( + 'MockSessionProvider sessions', + $provider->describe( \Language::factory( 'en' ) ) + ); + } + + public function testGetAllowedUserRights() { + $provider = $this->getMockForAbstractClass( SessionProvider::class ); + $backend = TestUtils::getDummySessionBackend(); + + try { + $provider->getAllowedUserRights( $backend ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( + 'Backend\'s provider isn\'t $this', + $ex->getMessage() + ); + } + + TestingAccessWrapper::newFromObject( $backend )->provider = $provider; + $this->assertNull( $provider->getAllowedUserRights( $backend ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/SessionTest.php b/www/wiki/tests/phpunit/includes/session/SessionTest.php new file mode 100644 index 00000000..f84d435f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/SessionTest.php @@ -0,0 +1,373 @@ +<?php + +namespace MediaWiki\Session; + +use Psr\Log\LogLevel; +use MediaWikiTestCase; +use User; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @covers MediaWiki\Session\Session + */ +class SessionTest extends MediaWikiTestCase { + + public function testConstructor() { + $backend = TestUtils::getDummySessionBackend(); + TestingAccessWrapper::newFromObject( $backend )->requests = [ -1 => 'dummy' ]; + TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' ); + + $session = new Session( $backend, 42, new \TestLogger ); + $priv = TestingAccessWrapper::newFromObject( $session ); + $this->assertSame( $backend, $priv->backend ); + $this->assertSame( 42, $priv->index ); + + $request = new \FauxRequest(); + $priv2 = TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) ); + $this->assertSame( $backend, $priv2->backend ); + $this->assertNotSame( $priv->index, $priv2->index ); + $this->assertSame( $request, $priv2->getRequest() ); + } + + /** + * @dataProvider provideMethods + * @param string $m Method to test + * @param array $args Arguments to pass to the method + * @param bool $index Whether the backend method gets passed the index + * @param bool $ret Whether the method returns a value + */ + public function testMethods( $m, $args, $index, $ret ) { + $mock = $this->getMockBuilder( DummySessionBackend::class ) + ->setMethods( [ $m, 'deregisterSession' ] ) + ->getMock(); + $mock->expects( $this->once() )->method( 'deregisterSession' ) + ->with( $this->identicalTo( 42 ) ); + + $tmp = $mock->expects( $this->once() )->method( $m ); + $expectArgs = []; + if ( $index ) { + $expectArgs[] = $this->identicalTo( 42 ); + } + foreach ( $args as $arg ) { + $expectArgs[] = $this->identicalTo( $arg ); + } + $tmp = call_user_func_array( [ $tmp, 'with' ], $expectArgs ); + + $retval = new \stdClass; + $tmp->will( $this->returnValue( $retval ) ); + + $session = TestUtils::getDummySession( $mock, 42 ); + + if ( $ret ) { + $this->assertSame( $retval, call_user_func_array( [ $session, $m ], $args ) ); + } else { + $this->assertNull( call_user_func_array( [ $session, $m ], $args ) ); + } + + // Trigger Session destructor + $session = null; + } + + public static function provideMethods() { + return [ + [ 'getId', [], false, true ], + [ 'getSessionId', [], false, true ], + [ 'resetId', [], false, true ], + [ 'getProvider', [], false, true ], + [ 'isPersistent', [], false, true ], + [ 'persist', [], false, false ], + [ 'unpersist', [], false, false ], + [ 'shouldRememberUser', [], false, true ], + [ 'setRememberUser', [ true ], false, false ], + [ 'getRequest', [], true, true ], + [ 'getUser', [], false, true ], + [ 'getAllowedUserRights', [], false, true ], + [ 'canSetUser', [], false, true ], + [ 'setUser', [ new \stdClass ], false, false ], + [ 'suggestLoginUsername', [], true, true ], + [ 'shouldForceHTTPS', [], false, true ], + [ 'setForceHTTPS', [ true ], false, false ], + [ 'getLoggedOutTimestamp', [], false, true ], + [ 'setLoggedOutTimestamp', [ 123 ], false, false ], + [ 'getProviderMetadata', [], false, true ], + [ 'save', [], false, false ], + [ 'delaySave', [], false, true ], + [ 'renew', [], false, false ], + ]; + } + + public function testDataAccess() { + $session = TestUtils::getDummySession(); + $backend = TestingAccessWrapper::newFromObject( $session )->backend; + + $this->assertEquals( 1, $session->get( 'foo' ) ); + $this->assertEquals( 'zero', $session->get( 0 ) ); + $this->assertFalse( $backend->dirty ); + + $this->assertEquals( null, $session->get( 'null' ) ); + $this->assertEquals( 'default', $session->get( 'null', 'default' ) ); + $this->assertFalse( $backend->dirty ); + + $session->set( 'foo', 55 ); + $this->assertEquals( 55, $backend->data['foo'] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session->set( 1, 'one' ); + $this->assertEquals( 'one', $backend->data[1] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session->set( 1, 'one' ); + $this->assertFalse( $backend->dirty ); + + $this->assertTrue( $session->exists( 'foo' ) ); + $this->assertTrue( $session->exists( 1 ) ); + $this->assertFalse( $session->exists( 'null' ) ); + $this->assertFalse( $session->exists( 100 ) ); + $this->assertFalse( $backend->dirty ); + + $session->remove( 'foo' ); + $this->assertArrayNotHasKey( 'foo', $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + $session->remove( 1 ); + $this->assertArrayNotHasKey( 1, $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session->remove( 101 ); + $this->assertFalse( $backend->dirty ); + + $backend->data = [ 'a', 'b', '?' => 'c' ]; + $this->assertSame( 3, $session->count() ); + $this->assertSame( 3, count( $session ) ); + $this->assertFalse( $backend->dirty ); + + $data = []; + foreach ( $session as $key => $value ) { + $data[$key] = $value; + } + $this->assertEquals( $backend->data, $data ); + $this->assertFalse( $backend->dirty ); + + $this->assertEquals( $backend->data, iterator_to_array( $session ) ); + $this->assertFalse( $backend->dirty ); + } + + public function testArrayAccess() { + $logger = new \TestLogger; + $session = TestUtils::getDummySession( null, -1, $logger ); + $backend = TestingAccessWrapper::newFromObject( $session )->backend; + + $this->assertEquals( 1, $session['foo'] ); + $this->assertEquals( 'zero', $session[0] ); + $this->assertFalse( $backend->dirty ); + + $logger->setCollect( true ); + $this->assertEquals( null, $session['null'] ); + $logger->setCollect( false ); + $this->assertFalse( $backend->dirty ); + $this->assertSame( [ + [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $session['foo'] = 55; + $this->assertEquals( 55, $backend->data['foo'] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session[1] = 'one'; + $this->assertEquals( 'one', $backend->data[1] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session[1] = 'one'; + $this->assertFalse( $backend->dirty ); + + $session['bar'] = [ 'baz' => [] ]; + $session['bar']['baz']['quux'] = 2; + $this->assertEquals( [ 'baz' => [ 'quux' => 2 ] ], $backend->data['bar'] ); + + $logger->setCollect( true ); + $session['bar2']['baz']['quux'] = 3; + $logger->setCollect( false ); + $this->assertEquals( [ 'baz' => [ 'quux' => 3 ] ], $backend->data['bar2'] ); + $this->assertSame( [ + [ LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + $backend->dirty = false; + $this->assertTrue( isset( $session['foo'] ) ); + $this->assertTrue( isset( $session[1] ) ); + $this->assertFalse( isset( $session['null'] ) ); + $this->assertFalse( isset( $session['missing'] ) ); + $this->assertFalse( isset( $session[100] ) ); + $this->assertFalse( $backend->dirty ); + + unset( $session['foo'] ); + $this->assertArrayNotHasKey( 'foo', $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + unset( $session[1] ); + $this->assertArrayNotHasKey( 1, $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + unset( $session[101] ); + $this->assertFalse( $backend->dirty ); + } + + public function testClear() { + $session = TestUtils::getDummySession(); + $priv = TestingAccessWrapper::newFromObject( $session ); + + $backend = $this->getMockBuilder( DummySessionBackend::class ) + ->setMethods( [ 'canSetUser', 'setUser', 'save' ] ) + ->getMock(); + $backend->expects( $this->once() )->method( 'canSetUser' ) + ->will( $this->returnValue( true ) ); + $backend->expects( $this->once() )->method( 'setUser' ) + ->with( $this->callback( function ( $user ) { + return $user instanceof User && $user->isAnon(); + } ) ); + $backend->expects( $this->once() )->method( 'save' ); + $priv->backend = $backend; + $session->clear(); + $this->assertSame( [], $backend->data ); + $this->assertTrue( $backend->dirty ); + + $backend = $this->getMockBuilder( DummySessionBackend::class ) + ->setMethods( [ 'canSetUser', 'setUser', 'save' ] ) + ->getMock(); + $backend->data = []; + $backend->expects( $this->once() )->method( 'canSetUser' ) + ->will( $this->returnValue( true ) ); + $backend->expects( $this->once() )->method( 'setUser' ) + ->with( $this->callback( function ( $user ) { + return $user instanceof User && $user->isAnon(); + } ) ); + $backend->expects( $this->once() )->method( 'save' ); + $priv->backend = $backend; + $session->clear(); + $this->assertFalse( $backend->dirty ); + + $backend = $this->getMockBuilder( DummySessionBackend::class ) + ->setMethods( [ 'canSetUser', 'setUser', 'save' ] ) + ->getMock(); + $backend->expects( $this->once() )->method( 'canSetUser' ) + ->will( $this->returnValue( false ) ); + $backend->expects( $this->never() )->method( 'setUser' ); + $backend->expects( $this->once() )->method( 'save' ); + $priv->backend = $backend; + $session->clear(); + $this->assertSame( [], $backend->data ); + $this->assertTrue( $backend->dirty ); + } + + public function testTokens() { + $session = TestUtils::getDummySession(); + $priv = TestingAccessWrapper::newFromObject( $session ); + $backend = $priv->backend; + + $token = TestingAccessWrapper::newFromObject( $session->getToken() ); + $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data ); + $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] ); + $secret = $backend->data['wsTokenSecrets']['default']; + $this->assertSame( $secret, $token->secret ); + $this->assertSame( '', $token->salt ); + $this->assertTrue( $token->wasNew() ); + + $token = TestingAccessWrapper::newFromObject( $session->getToken( 'foo' ) ); + $this->assertSame( $secret, $token->secret ); + $this->assertSame( 'foo', $token->salt ); + $this->assertFalse( $token->wasNew() ); + + $backend->data['wsTokenSecrets']['secret'] = 'sekret'; + $token = TestingAccessWrapper::newFromObject( + $session->getToken( [ 'bar', 'baz' ], 'secret' ) + ); + $this->assertSame( 'sekret', $token->secret ); + $this->assertSame( 'bar|baz', $token->salt ); + $this->assertFalse( $token->wasNew() ); + + $session->resetToken( 'secret' ); + $this->assertArrayHasKey( 'wsTokenSecrets', $backend->data ); + $this->assertArrayHasKey( 'default', $backend->data['wsTokenSecrets'] ); + $this->assertArrayNotHasKey( 'secret', $backend->data['wsTokenSecrets'] ); + + $session->resetAllTokens(); + $this->assertArrayNotHasKey( 'wsTokenSecrets', $backend->data ); + } + + /** + * @dataProvider provideSecretsRoundTripping + * @param mixed $data + */ + public function testSecretsRoundTripping( $data ) { + $session = TestUtils::getDummySession(); + + // Simple round-trip + $session->setSecret( 'secret', $data ); + $this->assertNotEquals( $data, $session->get( 'secret' ) ); + $this->assertEquals( $data, $session->getSecret( 'secret', 'defaulted' ) ); + } + + public static function provideSecretsRoundTripping() { + return [ + [ 'Foobar' ], + [ 42 ], + [ [ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ], + [ (object)[ 'foo', 'bar' => 'baz', 'subarray' => [ 1, 2, 3 ] ] ], + [ true ], + [ false ], + [ null ], + ]; + } + + public function testSecrets() { + $logger = new \TestLogger; + $session = TestUtils::getDummySession( null, -1, $logger ); + + // Simple defaulting + $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) ); + + // Bad encrypted data + $session->set( 'test', 'foobar' ); + $logger->setCollect( true ); + $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) ); + $logger->setCollect( false ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Invalid sealed-secret format' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Tampered data + $session->setSecret( 'test', 'foobar' ); + $encrypted = $session->get( 'test' ); + $session->set( 'test', $encrypted . 'x' ); + $logger->setCollect( true ); + $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) ); + $logger->setCollect( false ); + $this->assertSame( [ + [ LogLevel::WARNING, 'Sealed secret has been tampered with, aborting.' ] + ], $logger->getBuffer() ); + $logger->clearBuffer(); + + // Unserializable data + $iv = \MWCryptRand::generate( 16, true ); + list( $encKey, $hmacKey ) = TestingAccessWrapper::newFromObject( $session )->getSecretKeys(); + $ciphertext = openssl_encrypt( 'foobar', 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv ); + $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext ); + $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true ); + $encrypted = base64_encode( $hmac ) . '.' . $sealed; + $session->set( 'test', $encrypted ); + \Wikimedia\suppressWarnings(); + $this->assertEquals( 'defaulted', $session->getSecret( 'test', 'defaulted' ) ); + \Wikimedia\restoreWarnings(); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php b/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php new file mode 100644 index 00000000..f9e30f06 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/TestBagOStuff.php @@ -0,0 +1,81 @@ +<?php + +namespace MediaWiki\Session; + +/** + * BagOStuff with utility functions for MediaWiki\\Session\\* testing + */ +class TestBagOStuff extends \CachedBagOStuff { + + public function __construct() { + parent::__construct( new \HashBagOStuff ); + } + + /** + * @param string $id Session ID + * @param array $data Session data + */ + public function setSessionData( $id, array $data ) { + $this->setSession( $id, [ 'data' => $data ] ); + } + + /** + * @param string $id Session ID + * @param array $metadata Session metadata + */ + public function setSessionMeta( $id, array $metadata ) { + $this->setSession( $id, [ 'metadata' => $metadata ] ); + } + + /** + * @param string $id Session ID + * @param array $blob Session metadata and data + */ + public function setSession( $id, array $blob ) { + $blob += [ + 'data' => [], + 'metadata' => [], + ]; + $blob['metadata'] += [ + 'userId' => 0, + 'userName' => null, + 'userToken' => null, + 'provider' => 'DummySessionProvider', + ]; + + $this->setRawSession( $id, $blob ); + } + + /** + * @param string $id Session ID + * @param array|mixed $blob Session metadata and data + */ + public function setRawSession( $id, $blob ) { + $expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' ); + $this->set( $this->makeKey( 'MWSession', $id ), $blob, $expiry ); + } + + /** + * @param string $id Session ID + * @return mixed + */ + public function getSession( $id ) { + return $this->get( $this->makeKey( 'MWSession', $id ) ); + } + + /** + * @param string $id Session ID + * @return mixed + */ + public function getSessionFromBackend( $id ) { + return $this->backend->get( $this->makeKey( 'MWSession', $id ) ); + } + + /** + * @param string $id Session ID + */ + public function deleteSession( $id ) { + $this->delete( $this->makeKey( 'MWSession', $id ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/TestUtils.php b/www/wiki/tests/phpunit/includes/session/TestUtils.php new file mode 100644 index 00000000..5db1ad0e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/TestUtils.php @@ -0,0 +1,106 @@ +<?php + +namespace MediaWiki\Session; + +use Psr\Log\LoggerInterface; +use Wikimedia\TestingAccessWrapper; + +/** + * Utility functions for Session unit tests + */ +class TestUtils { + + /** + * Override the singleton for unit testing + * @param SessionManager|null $manager + * @return \\Wikimedia\ScopedCallback|null + */ + public static function setSessionManagerSingleton( SessionManager $manager = null ) { + session_write_close(); + + $rInstance = new \ReflectionProperty( + SessionManager::class, 'instance' + ); + $rInstance->setAccessible( true ); + $rGlobalSession = new \ReflectionProperty( + SessionManager::class, 'globalSession' + ); + $rGlobalSession->setAccessible( true ); + $rGlobalSessionRequest = new \ReflectionProperty( + SessionManager::class, 'globalSessionRequest' + ); + $rGlobalSessionRequest->setAccessible( true ); + + $oldInstance = $rInstance->getValue(); + + $reset = [ + [ $rInstance, $oldInstance ], + [ $rGlobalSession, $rGlobalSession->getValue() ], + [ $rGlobalSessionRequest, $rGlobalSessionRequest->getValue() ], + ]; + + $rInstance->setValue( $manager ); + $rGlobalSession->setValue( null ); + $rGlobalSessionRequest->setValue( null ); + if ( $manager && PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( $manager ); + } + + return new \Wikimedia\ScopedCallback( function () use ( &$reset, $oldInstance ) { + foreach ( $reset as &$arr ) { + $arr[0]->setValue( $arr[1] ); + } + if ( $oldInstance && PHPSessionHandler::isInstalled() ) { + PHPSessionHandler::install( $oldInstance ); + } + } ); + } + + /** + * If you need a SessionBackend for testing but don't want to create a real + * one, use this. + * @return SessionBackend Unconfigured! Use reflection to set any private + * fields necessary. + */ + public static function getDummySessionBackend() { + $rc = new \ReflectionClass( SessionBackend::class ); + if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) { + \PHPUnit_Framework_Assert::markTestSkipped( + 'ReflectionClass::newInstanceWithoutConstructor isn\'t available' + ); + } + + $ret = $rc->newInstanceWithoutConstructor(); + TestingAccessWrapper::newFromObject( $ret )->logger = new \TestLogger; + return $ret; + } + + /** + * If you need a Session for testing but don't want to create a backend to + * construct one, use this. + * @param object $backend Object to serve as the SessionBackend + * @param int $index + * @param LoggerInterface $logger + * @return Session + */ + public static function getDummySession( $backend = null, $index = -1, $logger = null ) { + $rc = new \ReflectionClass( Session::class ); + if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) { + \PHPUnit_Framework_Assert::markTestSkipped( + 'ReflectionClass::newInstanceWithoutConstructor isn\'t available' + ); + } + + if ( $backend === null ) { + $backend = new DummySessionBackend; + } + + $session = $rc->newInstanceWithoutConstructor(); + $priv = TestingAccessWrapper::newFromObject( $session ); + $priv->backend = $backend; + $priv->index = $index; + $priv->logger = $logger ?: new \TestLogger; + return $session; + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/TokenTest.php b/www/wiki/tests/phpunit/includes/session/TokenTest.php new file mode 100644 index 00000000..47976527 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/TokenTest.php @@ -0,0 +1,67 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use Wikimedia\TestingAccessWrapper; + +/** + * @group Session + * @covers MediaWiki\Session\Token + */ +class TokenTest extends MediaWikiTestCase { + + public function testBasics() { + $token = $this->getMockBuilder( Token::class ) + ->setMethods( [ 'toStringAtTimestamp' ] ) + ->setConstructorArgs( [ 'sekret', 'salty', true ] ) + ->getMock(); + $token->expects( $this->any() )->method( 'toStringAtTimestamp' ) + ->will( $this->returnValue( 'faketoken+\\' ) ); + + $this->assertSame( 'faketoken+\\', $token->toString() ); + $this->assertSame( 'faketoken+\\', (string)$token ); + $this->assertTrue( $token->wasNew() ); + + $token = new Token( 'sekret', 'salty', false ); + $this->assertFalse( $token->wasNew() ); + } + + public function testToStringAtTimestamp() { + $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) ); + + $this->assertSame( + 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\', + $token->toStringAtTimestamp( 1447362018 ) + ); + $this->assertSame( + 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\', + $token->toStringAtTimestamp( 1447362026 ) + ); + } + + public function testGetTimestamp() { + $this->assertSame( + 1447362018, Token::getTimestamp( 'd9ade0c7d4349e9df9094e61c33a5a0d5644fde2+\\' ) + ); + $this->assertSame( + 1447362026, Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea+\\' ) + ); + $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) ); + $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9176c224cfb400d43be+\\' ) ); + + $this->assertNull( Token::getTimestamp( 'ee2f7a2488dea9x76c224cfb400d43be5644fdea+\\' ) ); + } + + public function testMatch() { + $token = TestingAccessWrapper::newFromObject( new Token( 'sekret', 'salty', false ) ); + + $test = $token->toStringAtTimestamp( time() - 10 ); + $this->assertTrue( $token->match( $test ) ); + $this->assertTrue( $token->match( $test, 12 ) ); + $this->assertFalse( $token->match( $test, 8 ) ); + + $this->assertFalse( $token->match( 'ee2f7a2488dea9176c224cfb400d43be5644fdea-\\' ) ); + } + +} diff --git a/www/wiki/tests/phpunit/includes/session/UserInfoTest.php b/www/wiki/tests/phpunit/includes/session/UserInfoTest.php new file mode 100644 index 00000000..4d79a956 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/session/UserInfoTest.php @@ -0,0 +1,186 @@ +<?php + +namespace MediaWiki\Session; + +use MediaWikiTestCase; +use User; + +/** + * @group Session + * @group Database + * @covers MediaWiki\Session\UserInfo + */ +class UserInfoTest extends MediaWikiTestCase { + + public function testNewAnonymous() { + $userinfo = UserInfo::newAnonymous(); + + $this->assertTrue( $userinfo->isAnon() ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( 0, $userinfo->getId() ); + $this->assertSame( null, $userinfo->getName() ); + $this->assertSame( '', $userinfo->getToken() ); + $this->assertNotNull( $userinfo->getUser() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + $this->assertSame( '<anon>', (string)$userinfo ); + } + + public function testNewFromId() { + $id = wfGetDB( DB_MASTER )->selectField( 'user', 'MAX(user_id)' ) + 1; + try { + UserInfo::newFromId( $id ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid ID', $ex->getMessage() ); + } + + $user = User::newFromName( 'UTSysop' ); + $userinfo = UserInfo::newFromId( $user->getId() ); + $this->assertFalse( $userinfo->isAnon() ); + $this->assertFalse( $userinfo->isVerified() ); + $this->assertSame( $user->getId(), $userinfo->getId() ); + $this->assertSame( $user->getName(), $userinfo->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo->getToken() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); + $userinfo2 = $userinfo->verified(); + $this->assertNotSame( $userinfo2, $userinfo ); + $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); + + $this->assertFalse( $userinfo2->isAnon() ); + $this->assertTrue( $userinfo2->isVerified() ); + $this->assertSame( $user->getId(), $userinfo2->getId() ); + $this->assertSame( $user->getName(), $userinfo2->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo2->getToken() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); + $this->assertSame( $userinfo2, $userinfo2->verified() ); + $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); + + $userinfo = UserInfo::newFromId( $user->getId(), true ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + } + + public function testNewFromName() { + try { + UserInfo::newFromName( '<bad name>' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid user name', $ex->getMessage() ); + } + + // User name that exists + $user = User::newFromName( 'UTSysop' ); + $userinfo = UserInfo::newFromName( $user->getName() ); + $this->assertFalse( $userinfo->isAnon() ); + $this->assertFalse( $userinfo->isVerified() ); + $this->assertSame( $user->getId(), $userinfo->getId() ); + $this->assertSame( $user->getName(), $userinfo->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo->getToken() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); + $userinfo2 = $userinfo->verified(); + $this->assertNotSame( $userinfo2, $userinfo ); + $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); + + $this->assertFalse( $userinfo2->isAnon() ); + $this->assertTrue( $userinfo2->isVerified() ); + $this->assertSame( $user->getId(), $userinfo2->getId() ); + $this->assertSame( $user->getName(), $userinfo2->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo2->getToken() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); + $this->assertSame( $userinfo2, $userinfo2->verified() ); + $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); + + $userinfo = UserInfo::newFromName( $user->getName(), true ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + + // User name that does not exist should still be non-anon + $user = User::newFromName( 'DoesNotExist' ); + $this->assertSame( 0, $user->getId(), 'sanity check' ); + $userinfo = UserInfo::newFromName( $user->getName() ); + $this->assertFalse( $userinfo->isAnon() ); + $this->assertFalse( $userinfo->isVerified() ); + $this->assertSame( $user->getId(), $userinfo->getId() ); + $this->assertSame( $user->getName(), $userinfo->getName() ); + $this->assertSame( '', $userinfo->getToken() ); + $this->assertInstanceOf( User::class, $userinfo->getUser() ); + $userinfo2 = $userinfo->verified(); + $this->assertNotSame( $userinfo2, $userinfo ); + $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); + + $this->assertFalse( $userinfo2->isAnon() ); + $this->assertTrue( $userinfo2->isVerified() ); + $this->assertSame( $user->getId(), $userinfo2->getId() ); + $this->assertSame( $user->getName(), $userinfo2->getName() ); + $this->assertSame( '', $userinfo2->getToken() ); + $this->assertInstanceOf( User::class, $userinfo2->getUser() ); + $this->assertSame( $userinfo2, $userinfo2->verified() ); + $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); + + $userinfo = UserInfo::newFromName( $user->getName(), true ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + } + + public function testNewFromUser() { + // User that exists + $user = User::newFromName( 'UTSysop' ); + $userinfo = UserInfo::newFromUser( $user ); + $this->assertFalse( $userinfo->isAnon() ); + $this->assertFalse( $userinfo->isVerified() ); + $this->assertSame( $user->getId(), $userinfo->getId() ); + $this->assertSame( $user->getName(), $userinfo->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo->getToken() ); + $this->assertSame( $user, $userinfo->getUser() ); + $userinfo2 = $userinfo->verified(); + $this->assertNotSame( $userinfo2, $userinfo ); + $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); + + $this->assertFalse( $userinfo2->isAnon() ); + $this->assertTrue( $userinfo2->isVerified() ); + $this->assertSame( $user->getId(), $userinfo2->getId() ); + $this->assertSame( $user->getName(), $userinfo2->getName() ); + $this->assertSame( $user->getToken( true ), $userinfo2->getToken() ); + $this->assertSame( $user, $userinfo2->getUser() ); + $this->assertSame( $userinfo2, $userinfo2->verified() ); + $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); + + $userinfo = UserInfo::newFromUser( $user, true ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + + // User name that does not exist should still be non-anon + $user = User::newFromName( 'DoesNotExist' ); + $this->assertSame( 0, $user->getId(), 'sanity check' ); + $userinfo = UserInfo::newFromUser( $user ); + $this->assertFalse( $userinfo->isAnon() ); + $this->assertFalse( $userinfo->isVerified() ); + $this->assertSame( $user->getId(), $userinfo->getId() ); + $this->assertSame( $user->getName(), $userinfo->getName() ); + $this->assertSame( '', $userinfo->getToken() ); + $this->assertSame( $user, $userinfo->getUser() ); + $userinfo2 = $userinfo->verified(); + $this->assertNotSame( $userinfo2, $userinfo ); + $this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo ); + + $this->assertFalse( $userinfo2->isAnon() ); + $this->assertTrue( $userinfo2->isVerified() ); + $this->assertSame( $user->getId(), $userinfo2->getId() ); + $this->assertSame( $user->getName(), $userinfo2->getName() ); + $this->assertSame( '', $userinfo2->getToken() ); + $this->assertSame( $user, $userinfo2->getUser() ); + $this->assertSame( $userinfo2, $userinfo2->verified() ); + $this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 ); + + $userinfo = UserInfo::newFromUser( $user, true ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( $userinfo, $userinfo->verified() ); + + // Anonymous user gives anon + $userinfo = UserInfo::newFromUser( new User, false ); + $this->assertTrue( $userinfo->isVerified() ); + $this->assertSame( 0, $userinfo->getId() ); + $this->assertSame( null, $userinfo->getName() ); + } + +} |