set(MemberAuthenticator::class, 'authenticators', []); Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class); // Set the default authenticator to use for these tests Injector::inst()->load([ Security::class => [ 'properties' => [ 'Authenticators' => [ 'default' => '%$' . MemberAuthenticator::class, ], ], ], ]); Member::config()->set('unique_identifier_field', 'Email'); PasswordValidator::config() ->remove('min_length') ->remove('historic_count') ->remove('min_test_score'); Member::set_password_validator(null); parent::setUp(); Director::config()->set('alternate_base_url', '/'); } public function testAccessingAuthenticatedPageRedirectsToLoginForm() { $this->autoFollowRedirection = false; $response = $this->get('SecurityTest_SecuredController'); $this->assertEquals(302, $response->getStatusCode()); $this->assertStringContainsString( Config::inst()->get(Security::class, 'login_url'), $response->getHeader('Location') ); $this->logInWithPermission('ADMIN'); $response = $this->get('SecurityTest_SecuredController'); $this->assertEquals(200, $response->getStatusCode()); $this->assertStringContainsString('Success', $response->getBody()); $this->autoFollowRedirection = true; } public function testPermissionFailureSetsCorrectFormMessages() { // Controller that doesn't attempt redirections $controller = new SecurityTest\NullController(); $controller->setRequest(Controller::curr()->getRequest()); $controller->setResponse(new HTTPResponse()); $session = Controller::curr()->getRequest()->getSession(); Security::permissionFailure($controller, ['default' => 'Oops, not allowed']); $this->assertEquals('Oops, not allowed', $session->get('Security.Message.message')); // Test that config values are used correctly Config::modify()->set(Security::class, 'default_message_set', 'stringvalue'); Security::permissionFailure($controller); $this->assertEquals( 'stringvalue', $session->get('Security.Message.message'), 'Default permission failure message value was not present' ); Config::modify()->remove(Security::class, 'default_message_set'); Config::modify()->merge(Security::class, 'default_message_set', ['default' => 'arrayvalue']); Security::permissionFailure($controller); $this->assertEquals( 'arrayvalue', $session->get('Security.Message.message'), 'Default permission failure message value was not present' ); // Test that non-default messages work. // NOTE: we inspect the response body here as the session message has already // been fetched and output as part of it, so has been removed from the session $this->logInWithPermission('EDITOR'); Config::modify()->set( Security::class, 'default_message_set', ['default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!'] ); Security::permissionFailure($controller); $this->assertStringContainsString( 'You are already logged in!', $controller->getResponse()->getBody(), 'Custom permission failure message was ignored' ); Security::permissionFailure( $controller, ['default' => 'default', 'alreadyLoggedIn' => 'One-off failure message'] ); $this->assertStringContainsString( 'One-off failure message', $controller->getResponse()->getBody(), "Message set passed to Security::permissionFailure() didn't override Config values" ); // Test DBField cast messages work Security::permissionFailure( $controller, DBField::create_field('HTMLFragment', '

Custom HTML & Message

') ); $this->assertStringContainsString( '

Custom HTML & Message

', $controller->getResponse()->getBody() ); // Plain text DBText Security::permissionFailure( $controller, DBField::create_field('Text', 'Safely escaped & message') ); $this->assertStringContainsString( 'Safely escaped & message', $controller->getResponse()->getBody() ); } /** * Follow all redirects recursively * * @param string $url * @param int $limit Max number of requests * @return HTTPResponse */ protected function getRecursive($url, $limit = 10) { $this->cssParser = null; $response = $this->mainSession->get($url); while (--$limit > 0 && $response instanceof HTTPResponse && $response->getHeader('Location')) { $response = $this->mainSession->followRedirection(); } return $response; } public function testAutomaticRedirectionOnLogin() { // BackURL with permission error (not authenticated) should not redirect if ($member = Security::getCurrentUser()) { Security::setCurrentUser(null); } $response = $this->getRecursive('SecurityTest_SecuredController'); $this->assertStringContainsString(Convert::raw2xml("That page is secured."), $response->getBody()); $this->assertStringContainsString('getBody()); // Non-logged in user should not be redirected, but instead shown the login form // No message/context is available as the user has not attempted to view the secured controller $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/'); $this->assertStringNotContainsString(Convert::raw2xml("That page is secured."), $response->getBody()); $this->assertStringNotContainsString(Convert::raw2xml("You don't have access to this page"), $response->getBody()); $this->assertStringContainsString('getBody()); // BackURL with permission error (wrong permissions) should not redirect $this->logInAs('grouplessmember'); $response = $this->getRecursive('SecurityTest_SecuredController'); $this->assertStringContainsString(Convert::raw2xml("You don't have access to this page"), $response->getBody()); $this->assertStringContainsString( 'getBody() ); // Directly accessing this page should attempt to follow the BackURL, but stop when it encounters the error $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/'); $this->assertStringContainsString(Convert::raw2xml("You don't have access to this page"), $response->getBody()); $this->assertStringContainsString( 'getBody() ); // Check correctly logged in admin doesn't generate the same errors $this->logInAs('admin'); $response = $this->getRecursive('SecurityTest_SecuredController'); $this->assertStringContainsString(Convert::raw2xml("Success"), $response->getBody()); // Directly accessing this page should attempt to follow the BackURL and succeed $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/'); $this->assertStringContainsString(Convert::raw2xml("Success"), $response->getBody()); } public function testLogInAsSomeoneElse() { $member = DataObject::get_one(Member::class); /* Log in with any user that we can find */ Security::setCurrentUser($member); /* View the Security/login page */ $this->get(Config::inst()->get(Security::class, 'login_url')); $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action'); /* We have only 1 input, one to allow the user to log in as someone else */ $this->assertEquals(count($items ?? []), 1, 'There is 1 input, allowing the user to log in as someone else.'); /* Submit the form, using only the logout action and a hidden field for the authenticator */ $response = $this->submitForm( 'MemberLoginForm_LoginForm', null, [ 'action_logout' => 1, ] ); /* We get a good response */ $this->assertEquals($response->getStatusCode(), 302, 'We have a redirection response'); /* Log the user out */ Security::setCurrentUser(null); } public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin() { // Attempt to fake a log in with a Member ID that doesn't exist in the DB // Note: attempting $this->logInAs(500) will throw a TypeError in RequestAuthenticationHandler::logIn() $this->session()->set('loggedInAs', 500); $this->autoFollowRedirection = true; /* Attempt to get into the admin section */ $this->get(Config::inst()->get(Security::class, 'login_url')); $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text'); /* We have 2 text inputs - one for email, and another for the password */ $this->assertEquals(count($items ?? []), 2, 'There are 2 inputs - one for email, another for password'); $this->autoFollowRedirection = false; /* Log the user out */ $this->logOut(); } public function testLoginUsernamePersists() { // Test that username does not persist $this->session()->set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com'); Security::config()->set('remember_username', false); $this->get(Config::inst()->get(Security::class, 'login_url')); $items = $this ->cssParser() ->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email'); $this->assertEquals(1, count($items ?? [])); $this->assertEmpty((string)$items[0]->attributes()->value); $this->assertEquals('off', (string)$items[0]->attributes()->autocomplete); $form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm'); $this->assertEquals(1, count($form ?? [])); $this->assertEquals('off', (string)$form[0]->attributes()->autocomplete); // Test that username does persist when necessary $this->session()->set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com'); Security::config()->set('remember_username', true); $this->get(Config::inst()->get(Security::class, 'login_url')); $items = $this ->cssParser() ->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email'); $this->assertEquals(1, count($items ?? [])); $this->assertEquals('myuser@silverstripe.com', (string)$items[0]->attributes()->value); $this->assertNotEquals('off', (string)$items[0]->attributes()->autocomplete); $form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm'); $this->assertEquals(1, count($form ?? [])); $this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete); } public function testLogout() { /* Enable SecurityToken */ $securityTokenWasEnabled = SecurityToken::is_enabled(); SecurityToken::enable(); $member = DataObject::get_one(Member::class); /* Log in with any user that we can find */ $this->logInAs($member); /* Visit the Security/logout page with a test referer, but without a security token */ $this->get( Config::inst()->get(Security::class, 'logout_url'), null, ['Referer' => Controller::join_links(Director::absoluteBaseURL(), 'testpage')] ); /* Make sure the user is still logged in */ $this->assertNotNull(Security::getCurrentUser(), 'User is still logged in.'); $token = $this->cssParser()->getBySelector('#LogoutForm_Form #LogoutForm_Form_SecurityID'); $actions = $this->cssParser()->getBySelector('#LogoutForm_Form input.action'); /* We have a security token, and an action to allow the user to log out */ $this->assertCount(1, $token, 'There is a hidden field containing a security token.'); $this->assertCount(1, $actions, 'There is 1 action, allowing the user to log out.'); /* Submit the form, using the logout action */ $response = $this->submitForm( 'LogoutForm_Form', null, [ 'action_doLogout' => 1, ] ); /* We get a good response */ $this->assertEquals(302, $response->getStatusCode()); $this->assertMatchesRegularExpression( '/testpage/', $response->getHeader('Location'), "Logout form redirects to back to referer." ); /* User is logged out successfully */ $this->assertNull(Security::getCurrentUser(), 'User is logged out.'); /* Re-disable SecurityToken */ if (!$securityTokenWasEnabled) { SecurityToken::disable(); } } public function testExternalBackUrlRedirectionDisallowed() { // Test internal relative redirect $response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'testpage'); $this->assertEquals(302, $response->getStatusCode()); $this->assertMatchesRegularExpression( '/testpage/', $response->getHeader('Location'), "Internal relative BackURLs work when passed through to login form" ); // Log the user out $this->logOut(); // Test internal absolute redirect $response = $this->doTestLoginForm( 'noexpiry@silverstripe.com', '1nitialPassword', Controller::join_links(Director::absoluteBaseURL(), 'testpage') ); // for some reason the redirect happens to a relative URL $this->assertMatchesRegularExpression( '/^' . preg_quote(Controller::join_links(Director::absoluteBaseURL(), 'testpage'), '/') . '/', $response->getHeader('Location'), "Internal absolute BackURLs work when passed through to login form" ); // Log the user out $this->logOut(); // Test external redirect $response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'http://myspoofedhost.com'); $this->assertDoesNotMatchRegularExpression( '/^' . preg_quote('http://myspoofedhost.com', '/') . '/', (string)$response->getHeader('Location'), "Redirection to external links in login form BackURL gets prevented as a measure against spoofing attacks" ); // Test external redirection on ChangePasswordForm $this->get('Security/changepassword?BackURL=http://myspoofedhost.com'); $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123'); $this->assertDoesNotMatchRegularExpression( '/^' . preg_quote('http://myspoofedhost.com', '/') . '/', (string)$changedResponse->getHeader('Location'), "Redirection to external links in change password form BackURL gets prevented to stop spoofing attacks" ); // Log the user out $this->logOut(); } /** * Test that the login form redirects to the change password form after logging in with an expired password */ public function testExpiredPassword() { /* BAD PASSWORDS ARE LOCKED OUT */ $badResponse = $this->doTestLoginForm('testuser@example.com', 'badpassword'); $this->assertEquals(302, $badResponse->getStatusCode()); $this->assertMatchesRegularExpression('/Security\/login/', $badResponse->getHeader('Location')); $this->assertNull($this->session()->get('loggedInAs')); /* UNEXPIRED PASSWORD GO THROUGH WITHOUT A HITCH */ $goodResponse = $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals( Controller::join_links(Director::absoluteBaseURL(), 'test/link'), $goodResponse->getHeader('Location') ); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs')); $this->logOut(); /* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */ $expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword'); $this->assertEquals(302, $expiredResponse->getStatusCode()); $this->assertEquals( Director::absoluteURL('Security/changepassword') . '?BackURL=test%2Flink', Director::absoluteURL((string) $expiredResponse->getHeader('Location')) ); $this->assertEquals( $this->idFromFixture(Member::class, 'expiredpassword'), $this->session()->get('loggedInAs') ); // Make sure it redirects correctly after the password has been changed $this->mainSession->followRedirection(); $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123'); $this->assertEquals(302, $changedResponse->getStatusCode()); $this->assertEquals( Controller::join_links(Director::absoluteBaseURL(), 'test/link'), $changedResponse->getHeader('Location') ); } public function testChangePasswordForLoggedInUsers() { $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); // Change the password $this->get('Security/changepassword?BackURL=test/back'); $changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123'); $this->assertEquals(302, $changedResponse->getStatusCode()); $this->assertEquals( Controller::join_links(Director::absoluteBaseURL(), 'test/back'), $changedResponse->getHeader('Location') ); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs')); // Check if we can login with the new password $this->logOut(); $goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword#123'); $this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals( Controller::join_links(Director::absoluteBaseURL(), 'test/link'), $goodResponse->getHeader('Location') ); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs')); } public function testChangePasswordFromLostPassword() { /** @var Member $admin */ $admin = $this->objFromFixture(Member::class, 'test'); $admin->FailedLoginCount = 99; $admin->LockedOutUntil = DBDatetime::now()->getValue(); $admin->write(); $this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password'); // Request new password by email $this->get('Security/lostpassword'); $this->post('Security/lostpassword/LostPasswordForm', ['Email' => 'testuser@example.com']); $this->assertEmailSent('testuser@example.com'); // Load password link from email $admin = DataObject::get_by_id(Member::class, $admin->ID); $this->assertNotNull($admin->AutoLoginHash, 'Hash has been written after lost password'); // We don't have access to the token - generate a new token and hash pair. $token = $admin->generateAutologinTokenAndStoreHash(); // Check. $response = $this->get('Security/changepassword/?m=' . $admin->ID . '&t=' . $token); $this->assertEquals(302, $response->getStatusCode()); $this->assertEquals( Director::absoluteURL('Security/changepassword'), Director::absoluteURL((string) $response->getHeader('Location')) ); // Follow redirection to form without hash in GET parameter $this->get('Security/changepassword'); $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword#123'); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs')); // Check if we can login with the new password $this->logOut(); $goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword#123'); $this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs')); $admin = DataObject::get_by_id(Member::class, $admin->ID, false); $this->assertNull($admin->LockedOutUntil); $this->assertEquals(0, $admin->FailedLoginCount); } public function testRepeatedLoginAttemptsLockingPeopleOut() { i18n::set_locale('en_US'); Member::config()->set('lock_out_after_incorrect_logins', 5); Member::config()->set('lock_out_delay_mins', 15); DBDatetime::set_mock_now('2017-05-22 00:00:00'); // Login with a wrong password for more than the defined threshold /** @var Member $member */ $member = null; for ($i = 1; $i <= 6; $i++) { $this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); /** @var Member $member */ $member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); if ($i < 5) { $this->assertNull( $member->LockedOutUntil, 'User does not have a lockout time set if under threshold for failed attempts' ); $this->assertHasMessage( _t( 'SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.' ) ); } else { // Lockout should be exactly 15 minutes from now /** @var DBDatetime $lockedOutUntilObj */ $lockedOutUntilObj = $member->dbObject('LockedOutUntil'); $this->assertEquals( DBDatetime::now()->getTimestamp() + (15 * 60), $lockedOutUntilObj->getTimestamp(), 'User has a lockout time set after too many failed attempts' ); } } $msg = _t( 'SilverStripe\\Security\\Member.ERRORLOCKEDOUT2', 'Your account has been temporarily disabled because of too many failed attempts at ' . 'logging in. Please try again in {count} minutes.', null, ['count' => 15] ); $this->assertHasMessage($msg); $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->assertNull( $this->session()->get('loggedInAs'), 'The user can\'t log in after being locked out, even with the right password' ); // Move into the future so we can login again DBDatetime::set_mock_now('2017-06-22 00:00:00'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->assertEquals( $member->ID, $this->session()->get('loggedInAs'), 'After lockout expires, the user can login again' ); // Log the user out $this->logOut(); // Login again with wrong password, but less attempts than threshold for ($i = 1; $i < 5; $i++) { $this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); } $this->assertNull($this->session()->get('loggedInAs')); $this->assertHasMessage( _t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'), 'The user can retry with a wrong password after the lockout expires' ); $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->assertEquals( $this->session()->get('loggedInAs'), $member->ID, 'The user can login successfully after lockout expires, if staying below the threshold' ); } public function testAlternatingRepeatedLoginAttempts() { Member::config()->set('lock_out_after_incorrect_logins', 3); // ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT $this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword'); $this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword'); /** @var Member $member1 */ $member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); /** @var Member $member2 */ $member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry')); $this->assertNull($member1->LockedOutUntil); $this->assertNull($member2->LockedOutUntil); // BUT, DOING AN ADDITIONAL LOG-IN WITH EITHER OF THEM WILL LOCK OUT, SINCE THAT IS THE 3RD FAILURE IN // THIS SESSION $this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); $this->assertNotNull($member1->LockedOutUntil); $this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword'); $member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry')); $this->assertNotNull($member2->LockedOutUntil); } public function testUnsuccessfulLoginAttempts() { Security::config()->set('login_recording', true); /* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */ $this->doTestLoginForm('testuser@example.com', 'wrongpassword'); /** @var LoginAttempt $attempt */ $attempt = LoginAttempt::getByEmail('testuser@example.com')->first(); $this->assertInstanceOf(LoginAttempt::class, $attempt); $member = Member::get()->filter('Email', 'testuser@example.com')->first(); $this->assertEquals($attempt->Status, 'Failure'); $this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data $this->assertEquals($attempt->EmailHashed, sha1('testuser@example.com')); $this->assertEquals($attempt->Member()->toMap(), $member->toMap()); /* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */ $this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword'); $attempt = LoginAttempt::getByEmail('wronguser@silverstripe.com')->first(); $this->assertInstanceOf(LoginAttempt::class, $attempt); $this->assertEquals($attempt->Status, 'Failure'); $this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data $this->assertEquals($attempt->EmailHashed, sha1('wronguser@silverstripe.com')); $this->assertNotEmpty($this->getValidationResult()->getMessages(), 'An invalid email returns a message.'); } public function testSuccessfulLoginAttempts() { Security::config()->set('login_recording', true); /* SUCCESSFUL ATTEMPTS ARE LOGGED */ $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); /** @var LoginAttempt $attempt */ $attempt = LoginAttempt::getByEmail('testuser@example.com')->first(); $member = Member::get()->filter('Email', 'testuser@example.com')->first(); $this->assertInstanceOf(LoginAttempt::class, $attempt); $this->assertEquals($attempt->Status, 'Success'); $this->assertEmpty($attempt->Email); // Doesn't store potentially sensitive data $this->assertEquals($attempt->EmailHashed, sha1('testuser@example.com')); $this->assertEquals($attempt->Member()->toMap(), $member->toMap()); } public function testDatabaseIsReadyWithInsufficientMemberColumns() { Security::clear_database_is_ready(); DBEnum::flushCache(); // Assumption: The database has been built correctly by the test runner, // and has all columns present in the ORM DB::get_schema()->renameField('Member', 'Email', 'Email_renamed'); // Email column is now missing, which means we're not ready to do permission checks $this->assertFalse(Security::database_is_ready()); // Rebuild the database (which re-adds the Email column), and try again static::resetDBSchema(true); $this->assertTrue(Security::database_is_ready()); } public function testSecurityControllerSendsRobotsTagHeader() { $response = $this->get(Config::inst()->get(Security::class, 'login_url')); $robotsHeader = $response->getHeader('X-Robots-Tag'); $this->assertNotNull($robotsHeader); $this->assertStringContainsString('noindex', $robotsHeader); } public function testDoNotSendEmptyRobotsHeaderIfNotDefined() { Config::modify()->remove(Security::class, 'robots_tag'); $response = $this->get(Config::inst()->get(Security::class, 'login_url')); $robotsHeader = $response->getHeader('X-Robots-Tag'); $this->assertNull($robotsHeader); } public function testGetResponseController() { if (!class_exists(Page::class)) { $this->markTestSkipped("This test requires CMS module"); } $request = new HTTPRequest('GET', '/'); $request->setSession(new Session([])); $security = new Security(); $security->setRequest($request); $reflection = new \ReflectionClass($security); $method = $reflection->getMethod('getResponseController'); $method->setAccessible(true); $result = $method->invoke($security, 'Page'); // Ensure page shares the same controller as security $securityClass = Config::inst()->get(Security::class, 'page_class'); /** @var Page $securityPage */ $securityPage = new $securityClass(); $this->assertInstanceOf($securityPage->getControllerName(), $result); $this->assertEquals($request, $result->getRequest()); } /** * Execute a log-in form using Director::test(). * Helper method for the tests above * * @param string $email * @param string $password * @param string $backURL * @return HTTPResponse */ public function doTestLoginForm($email, $password, $backURL = 'test/link') { $this->get(Config::inst()->get(Security::class, 'logout_url')); $this->session()->set('BackURL', $backURL); $this->get(Config::inst()->get(Security::class, 'login_url')); return $this->submitForm( "MemberLoginForm_LoginForm", null, [ 'Email' => $email, 'Password' => $password, 'AuthenticationMethod' => MemberAuthenticator::class, 'action_doLogin' => 1, ] ); } /** * Helper method to execute a change password form * * @param string $oldPassword * @param string $newPassword * @return HTTPResponse */ public function doTestChangepasswordForm($oldPassword, $newPassword) { return $this->submitForm( "ChangePasswordForm_ChangePasswordForm", null, [ 'OldPassword' => $oldPassword, 'NewPassword1' => $newPassword, 'NewPassword2' => $newPassword, 'action_doChangePassword' => 1, ] ); } /** * Assert this message is in the current login form errors * * @param string $expected * @param string $errorMessage */ protected function assertHasMessage($expected, $errorMessage = null) { $messages = []; $result = $this->getValidationResult(); if ($result) { foreach ($result->getMessages() as $message) { $messages[] = $message['message']; } } $this->assertContains($expected, $messages, $errorMessage ?: ''); } /** * Get validation result from last login form submission * * @return ValidationResult */ protected function getValidationResult() { $result = $this->session()->get('FormInfo.MemberLoginForm_LoginForm.result'); if ($result) { return unserialize($result ?? ''); } return null; } }