Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

SecurityTest::tearDown()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 0
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\ORM\FieldType\DBDatetime;
7
use SilverStripe\ORM\FieldType\DBClassName;
8
use SilverStripe\ORM\DB;
9
use SilverStripe\ORM\ValidationResult;
10
use SilverStripe\Security\Authenticator;
11
use SilverStripe\Security\LoginAttempt;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\MemberAuthenticator;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\Permission;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Convert;
18
use SilverStripe\Dev\FunctionalTest;
19
use SilverStripe\Dev\TestOnly;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Control\Session;
22
use SilverStripe\Control\Director;
23
use SilverStripe\Control\Controller;
24
use SilverStripe\i18n\i18n;
25
26
/**
27
 * Test the security class, including log-in form, change password form, etc
28
 */
29
class SecurityTest extends FunctionalTest {
30
	protected static $fixture_file = 'MemberTest.yml';
31
32
	protected $autoFollowRedirection = false;
33
34
	protected $priorAuthenticators = array();
35
36
	protected $priorDefaultAuthenticator = null;
37
38
	protected $priorUniqueIdentifierField = null;
39
40
	protected $priorRememberUsername = null;
41
42
	protected $extraControllers = [
43
		SecurityTest\NullController::class,
44
		SecurityTest\SecuredController::class,
45
	];
46
47
	public function setUp() {
48
		// This test assumes that MemberAuthenticator is present and the default
49
		$this->priorAuthenticators = Authenticator::get_authenticators();
50
		$this->priorDefaultAuthenticator = Authenticator::get_default_authenticator();
51
		foreach($this->priorAuthenticators as $authenticator) {
52
			Authenticator::unregister($authenticator);
53
		}
54
55
		Authenticator::register(MemberAuthenticator::class);
56
		Authenticator::set_default_authenticator(MemberAuthenticator::class);
57
58
		// And that the unique identified field is 'Email'
59
		$this->priorUniqueIdentifierField = Member::config()->unique_identifier_field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
60
		$this->priorRememberUsername = Security::config()->remember_username;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
61
		/** @skipUpgrade */
62
		Member::config()->unique_identifier_field = 'Email';
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
63
64
		parent::setUp();
65
66
		Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/');
67
	}
68
69
	public function tearDown() {
70
		// Restore selected authenticator
71
72
		// MemberAuthenticator might not actually be present
73
		if(!in_array(MemberAuthenticator::class, $this->priorAuthenticators)) {
74
			Authenticator::unregister(MemberAuthenticator::class);
75
		}
76
		foreach($this->priorAuthenticators as $authenticator) {
77
			Authenticator::register($authenticator);
78
		}
79
		Authenticator::set_default_authenticator($this->priorDefaultAuthenticator);
80
81
		// Restore unique identifier field
82
		Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
83
		Security::config()->remember_username = $this->priorRememberUsername;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
84
85
		parent::tearDown();
86
	}
87
88
	public function testAccessingAuthenticatedPageRedirectsToLoginForm() {
89
		$this->autoFollowRedirection = false;
90
91
		$response = $this->get('SecurityTest_SecuredController');
92
		$this->assertEquals(302, $response->getStatusCode());
93
		$this->assertContains(
94
			Config::inst()->get(Security::class, 'login_url'),
95
			$response->getHeader('Location')
96
		);
97
98
		$this->logInWithPermission('ADMIN');
99
		$response = $this->get('SecurityTest_SecuredController');
100
		$this->assertEquals(200, $response->getStatusCode());
101
		$this->assertContains('Success', $response->getBody());
102
103
		$this->autoFollowRedirection = true;
104
	}
105
106
	public function testPermissionFailureSetsCorrectFormMessages() {
107
		Config::nest();
108
109
		// Controller that doesn't attempt redirections
110
		$controller = new SecurityTest\NullController();
111
		$controller->setResponse(new HTTPResponse());
112
113
		Security::permissionFailure($controller, array('default' => 'Oops, not allowed'));
114
		$this->assertEquals('Oops, not allowed', Session::get('Security.Message.message'));
115
116
		// Test that config values are used correctly
117
		Config::inst()->update(Security::class, 'default_message_set', 'stringvalue');
118
		Security::permissionFailure($controller);
119
		$this->assertEquals('stringvalue', Session::get('Security.Message.message'),
120
			'Default permission failure message value was not present');
121
122
		Config::inst()->remove(Security::class, 'default_message_set');
123
		Config::inst()->update(Security::class, 'default_message_set', array('default' => 'arrayvalue'));
124
		Security::permissionFailure($controller);
125
		$this->assertEquals('arrayvalue', Session::get('Security.Message.message'),
126
			'Default permission failure message value was not present');
127
128
		// Test that non-default messages work.
129
		// NOTE: we inspect the response body here as the session message has already
130
		// been fetched and output as part of it, so has been removed from the session
131
		$this->logInWithPermission('EDITOR');
132
133
		Config::inst()->update(Security::class, 'default_message_set',
134
			array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!'));
135
		Security::permissionFailure($controller);
136
		$this->assertContains('You are already logged in!', $controller->getResponse()->getBody(),
137
			'Custom permission failure message was ignored');
138
139
		Security::permissionFailure($controller,
140
			array('default' => 'default', 'alreadyLoggedIn' => 'One-off failure message'));
141
		$this->assertContains('One-off failure message', $controller->getResponse()->getBody(),
142
			"Message set passed to Security::permissionFailure() didn't override Config values");
143
144
		Config::unnest();
145
	}
146
147
	/**
148
	 * Follow all redirects recursively
149
	 *
150
	 * @param string $url
151
	 * @param int $limit Max number of requests
152
	 * @return HTTPResponse
153
	 */
154
	protected function getRecursive($url, $limit = 10) {
155
		$this->cssParser = null;
156
		$response = $this->mainSession->get($url);
157
		while(--$limit > 0 && $response instanceof HTTPResponse && $response->getHeader('Location')) {
158
			$response = $this->mainSession->followRedirection();
159
		}
160
		return $response;
161
	}
162
163
	public function testAutomaticRedirectionOnLogin() {
164
		// BackURL with permission error (not authenticated) should not redirect
165
		if($member = Member::currentUser()) $member->logOut();
166
		$response = $this->getRecursive('SecurityTest_SecuredController');
167
		$this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody());
168
		$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
169
170
		// Non-logged in user should not be redirected, but instead shown the login form
171
		// No message/context is available as the user has not attempted to view the secured controller
172
		$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
173
		$this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody());
174
		$this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
175
		$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
176
177
		// BackURL with permission error (wrong permissions) should not redirect
178
		$this->logInAs('grouplessmember');
179
		$response = $this->getRecursive('SecurityTest_SecuredController');
180
		$this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
181
		$this->assertContains(
182
			'<input type="submit" name="action_logout" value="Log in as someone else"',
183
			$response->getBody()
184
		);
185
186
		// Directly accessing this page should attempt to follow the BackURL, but stop when it encounters the error
187
		$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
188
		$this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
189
		$this->assertContains(
190
			'<input type="submit" name="action_logout" value="Log in as someone else"',
191
			$response->getBody()
192
		);
193
194
		// Check correctly logged in admin doesn't generate the same errors
195
		$this->logInAs('admin');
196
		$response = $this->getRecursive('SecurityTest_SecuredController');
197
		$this->assertContains(Convert::raw2xml("Success"), $response->getBody());
198
199
		// Directly accessing this page should attempt to follow the BackURL and succeed
200
		$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
201
		$this->assertContains(Convert::raw2xml("Success"), $response->getBody());
202
	}
203
204
	public function testLogInAsSomeoneElse() {
205
		$member = DataObject::get_one(Member::class);
206
207
		/* Log in with any user that we can find */
208
		$this->session()->inst_set('loggedInAs', $member->ID);
209
210
		/* View the Security/login page */
211
		$response = $this->get(Config::inst()->get(Security::class, 'login_url'));
212
213
		$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
214
215
		/* We have only 1 input, one to allow the user to log in as someone else */
216
		$this->assertEquals(count($items), 1, 'There is 1 input, allowing the user to log in as someone else.');
217
218
		$this->autoFollowRedirection = true;
219
220
		/* Submit the form, using only the logout action and a hidden field for the authenticator */
221
		$response = $this->submitForm(
222
			'MemberLoginForm_LoginForm',
223
			null,
224
			array(
225
				'AuthenticationMethod' => MemberAuthenticator::class,
226
				'action_dologout' => 1,
227
			)
228
		);
229
230
		/* We get a good response */
231
		$this->assertEquals($response->getStatusCode(), 200, 'We have a 200 OK response');
232
		$this->assertNotNull($response->getBody(), 'There is body content on the page');
233
234
		/* Log the user out */
235
		$this->session()->inst_set('loggedInAs', null);
236
	}
237
238
	public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin() {
239
		/* Log in with a Member ID that doesn't exist in the DB */
240
		$this->session()->inst_set('loggedInAs', 500);
241
242
		$this->autoFollowRedirection = true;
243
244
		/* Attempt to get into the admin section */
245
		$response = $this->get(Config::inst()->get(Security::class, 'login_url'));
246
247
		$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
248
249
		/* We have 2 text inputs - one for email, and another for the password */
250
		$this->assertEquals(count($items), 2, 'There are 2 inputs - one for email, another for password');
251
252
		$this->autoFollowRedirection = false;
253
254
		/* Log the user out */
255
		$this->session()->inst_set('loggedInAs', null);
256
	}
257
258
	public function testLoginUsernamePersists() {
259
		// Test that username does not persist
260
		$this->session()->inst_set('SessionForms.MemberLoginForm.Email', '[email protected]');
261
		Security::config()->remember_username = false;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
262
		$this->get(Config::inst()->get(Security::class, 'login_url'));
263
		$items = $this
264
			->cssParser()
265
			->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
266
		$this->assertEquals(1, count($items));
267
		$this->assertEmpty((string)$items[0]->attributes()->value);
268
		$this->assertEquals('off', (string)$items[0]->attributes()->autocomplete);
269
		$form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
270
		$this->assertEquals(1, count($form));
271
		$this->assertEquals('off', (string)$form[0]->attributes()->autocomplete);
272
273
		// Test that username does persist when necessary
274
		$this->session()->inst_set('SessionForms.MemberLoginForm.Email', '[email protected]');
275
		Security::config()->remember_username = true;
0 ignored issues
show
Documentation introduced by
The property remember_username does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
276
		$this->get(Config::inst()->get(Security::class, 'login_url'));
277
		$items = $this
278
			->cssParser()
279
			->getBySelector('#MemberLoginForm_LoginForm #MemberLoginForm_LoginForm_Email');
280
		$this->assertEquals(1, count($items));
281
		$this->assertEquals('[email protected]', (string)$items[0]->attributes()->value);
282
		$this->assertNotEquals('off', (string)$items[0]->attributes()->autocomplete);
283
		$form = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm');
284
		$this->assertEquals(1, count($form));
285
		$this->assertNotEquals('off', (string)$form[0]->attributes()->autocomplete);
286
	}
287
288
	public function testExternalBackUrlRedirectionDisallowed() {
289
		// Test internal relative redirect
290
		$response = $this->doTestLoginForm('[email protected]', '1nitialPassword', 'testpage');
291
		$this->assertEquals(302, $response->getStatusCode());
292
		$this->assertRegExp('/testpage/', $response->getHeader('Location'),
293
			"Internal relative BackURLs work when passed through to login form"
294
		);
295
		// Log the user out
296
		$this->session()->inst_set('loggedInAs', null);
297
298
		// Test internal absolute redirect
299
		$response = $this->doTestLoginForm('[email protected]', '1nitialPassword',
300
			Director::absoluteBaseURL() . 'testpage');
301
		// for some reason the redirect happens to a relative URL
302
		$this->assertRegExp('/^' . preg_quote(Director::absoluteBaseURL(), '/') . 'testpage/',
303
			$response->getHeader('Location'),
304
			"Internal absolute BackURLs work when passed through to login form"
305
		);
306
		// Log the user out
307
		$this->session()->inst_set('loggedInAs', null);
308
309
		// Test external redirect
310
		$response = $this->doTestLoginForm('[email protected]', '1nitialPassword', 'http://myspoofedhost.com');
311
		$this->assertNotRegExp('/^' . preg_quote('http://myspoofedhost.com', '/') . '/',
312
			(string)$response->getHeader('Location'),
313
			"Redirection to external links in login form BackURL gets prevented as a measure against spoofing attacks"
314
		);
315
316
		// Test external redirection on ChangePasswordForm
317
		$this->get('Security/changepassword?BackURL=http://myspoofedhost.com');
318
		$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
319
		$this->assertNotRegExp('/^' . preg_quote('http://myspoofedhost.com', '/') . '/',
320
			(string)$changedResponse->getHeader('Location'),
321
			"Redirection to external links in change password form BackURL gets prevented to stop spoofing attacks"
322
		);
323
324
		// Log the user out
325
		$this->session()->inst_set('loggedInAs', null);
326
	}
327
328
	/**
329
	 * Test that the login form redirects to the change password form after logging in with an expired password
330
	 */
331
	public function testExpiredPassword() {
332
		/* BAD PASSWORDS ARE LOCKED OUT */
333
		$badResponse = $this->doTestLoginForm('[email protected]' , 'badpassword');
334
		$this->assertEquals(302, $badResponse->getStatusCode());
335
		$this->assertRegExp('/Security\/login/', $badResponse->getHeader('Location'));
336
		$this->assertNull($this->session()->inst_get('loggedInAs'));
337
338
		/* UNEXPIRED PASSWORD GO THROUGH WITHOUT A HITCH */
339
		$goodResponse = $this->doTestLoginForm('[email protected]' , '1nitialPassword');
340
		$this->assertEquals(302, $goodResponse->getStatusCode());
341
		$this->assertEquals(
342
			Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
343
			$goodResponse->getHeader('Location')
344
		);
345
		$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
346
347
		/* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */
348
		$expiredResponse = $this->doTestLoginForm('[email protected]' , '1nitialPassword');
349
		$this->assertEquals(302, $expiredResponse->getStatusCode());
350
		$this->assertEquals(
351
			'/Security/changepassword',
352
			$expiredResponse->getHeader('Location')
353
		);
354
		$this->assertEquals($this->idFromFixture(Member::class, 'expiredpassword'),
355
			$this->session()->inst_get('loggedInAs'));
356
357
		// Make sure it redirects correctly after the password has been changed
358
		$this->mainSession->followRedirection();
359
		$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
360
		$this->assertEquals(302, $changedResponse->getStatusCode());
361
		$this->assertEquals(
362
			Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
363
			$changedResponse->getHeader('Location')
364
		);
365
	}
366
367
	public function testChangePasswordForLoggedInUsers() {
368
		$goodResponse = $this->doTestLoginForm('[email protected]' , '1nitialPassword');
369
370
		// Change the password
371
		$this->get('Security/changepassword?BackURL=test/back');
372
		$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
373
		$this->assertEquals(302, $changedResponse->getStatusCode());
374
		$this->assertEquals(
375
			Controller::join_links(Director::absoluteBaseURL(), 'test/back'),
376
			$changedResponse->getHeader('Location')
377
		);
378
		$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
379
380
		// Check if we can login with the new password
381
		$goodResponse = $this->doTestLoginForm('[email protected]' , 'changedPassword');
382
		$this->assertEquals(302, $goodResponse->getStatusCode());
383
		$this->assertEquals(
384
			Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
385
			$goodResponse->getHeader('Location')
386
		);
387
		$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
388
	}
389
390
	public function testChangePasswordFromLostPassword() {
391
		$admin = $this->objFromFixture(Member::class, 'test');
392
		$admin->FailedLoginCount = 99;
0 ignored issues
show
Documentation introduced by
The property FailedLoginCount does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
393
		$admin->LockedOutUntil = DBDatetime::now()->Format('Y-m-d H:i:s');
0 ignored issues
show
Documentation introduced by
The property LockedOutUntil does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
394
		$admin->write();
395
396
		$this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password');
397
398
		// Request new password by email
399
		$response = $this->get('Security/lostpassword');
400
		$response = $this->post('Security/LostPasswordForm', array('Email' => '[email protected]'));
401
402
		$this->assertEmailSent('[email protected]');
403
404
		// Load password link from email
405
		$admin = DataObject::get_by_id(Member::class, $admin->ID);
406
		$this->assertNotNull($admin->AutoLoginHash, 'Hash has been written after lost password');
407
408
		// We don't have access to the token - generate a new token and hash pair.
409
		$token = $admin->generateAutologinTokenAndStoreHash();
410
411
		// Check.
412
		$response = $this->get('Security/changepassword/?m='.$admin->ID.'&t=' . $token);
413
		$this->assertEquals(302, $response->getStatusCode());
414
		$this->assertEquals(Director::baseUrl() . 'Security/changepassword', $response->getHeader('Location'));
415
416
		// Follow redirection to form without hash in GET parameter
417
		$response = $this->get('Security/changepassword');
418
		$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
419
		$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
420
421
		// Check if we can login with the new password
422
		$goodResponse = $this->doTestLoginForm('[email protected]' , 'changedPassword');
423
		$this->assertEquals(302, $goodResponse->getStatusCode());
424
		$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
425
426
		$admin = DataObject::get_by_id(Member::class, $admin->ID, false);
427
		$this->assertNull($admin->LockedOutUntil);
428
		$this->assertEquals(0, $admin->FailedLoginCount);
429
	}
430
431
	public function testRepeatedLoginAttemptsLockingPeopleOut() {
432
		$local = i18n::get_locale();
433
		i18n::set_locale('en_US');
434
435
		Member::config()->lock_out_after_incorrect_logins = 5;
0 ignored issues
show
Documentation introduced by
The property lock_out_after_incorrect_logins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
436
		Member::config()->lock_out_delay_mins = 15;
0 ignored issues
show
Documentation introduced by
The property lock_out_delay_mins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
437
438
		// Login with a wrong password for more than the defined threshold
439
		for($i = 1; $i <= Member::config()->lock_out_after_incorrect_logins+1; $i++) {
440
			$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
441
			$member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
442
443
			if($i < Member::config()->lock_out_after_incorrect_logins) {
444
				$this->assertNull(
445
					$member->LockedOutUntil,
446
					'User does not have a lockout time set if under threshold for failed attempts'
447
				);
448
				$this->assertHasMessage(_t(
449
				    'Member.ERRORWRONGCRED',
450
                    'The provided details don\'t seem to be correct. Please try again.'
451
                ));
452
			} else {
453
				// Fuzzy matching for time to avoid side effects from slow running tests
454
				$this->assertGreaterThan(
455
					time() + 14*60,
456
					strtotime($member->LockedOutUntil),
457
					'User has a lockout time set after too many failed attempts'
458
				);
459
			}
460
461
			$msg = _t(
462
				'Member.ERRORLOCKEDOUT2',
463
				'Your account has been temporarily disabled because of too many failed attempts at ' .
464
				'logging in. Please try again in {count} minutes.',
465
				null,
466
				array('count' => Member::config()->lock_out_delay_mins)
467
			);
468
			if($i > Member::config()->lock_out_after_incorrect_logins) {
469
                $this->assertHasMessage($msg);
470
			}
471
		}
472
473
		$this->doTestLoginForm('[email protected]' , '1nitialPassword');
474
		$this->assertNull(
475
			$this->session()->inst_get('loggedInAs'),
476
			'The user can\'t log in after being locked out, even with the right password'
477
		);
478
479
		// (We fake this by re-setting LockedOutUntil)
480
		$member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
481
		$member->LockedOutUntil = date('Y-m-d H:i:s', time() - 30);
0 ignored issues
show
Documentation introduced by
The property LockedOutUntil does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
482
		$member->write();
483
		$this->doTestLoginForm('[email protected]' , '1nitialPassword');
484
		$this->assertEquals(
485
			$this->session()->inst_get('loggedInAs'),
486
			$member->ID,
487
			'After lockout expires, the user can login again'
488
		);
489
490
		// Log the user out
491
		$this->session()->inst_set('loggedInAs', null);
492
493
		// Login again with wrong password, but less attempts than threshold
494
		for($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {
495
			$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
496
		}
497
		$this->assertNull($this->session()->inst_get('loggedInAs'));
498
		$this->assertHasMessage(
499
		    _t('Member.ERRORWRONGCRED','The provided details don\'t seem to be correct. Please try again.'),
500
			'The user can retry with a wrong password after the lockout expires'
501
		);
502
503
		$this->doTestLoginForm('[email protected]' , '1nitialPassword');
504
		$this->assertEquals(
505
			$this->session()->inst_get('loggedInAs'),
506
			$member->ID,
507
			'The user can login successfully after lockout expires, if staying below the threshold'
508
		);
509
510
		i18n::set_locale($local);
511
	}
512
513
	public function testAlternatingRepeatedLoginAttempts() {
514
		Member::config()->lock_out_after_incorrect_logins = 3;
0 ignored issues
show
Documentation introduced by
The property lock_out_after_incorrect_logins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
515
516
		// ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT
517
518
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
519
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
520
521
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
522
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
523
524
		$member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
525
		$member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry'));
526
527
		$this->assertNull($member1->LockedOutUntil);
528
		$this->assertNull($member2->LockedOutUntil);
529
530
		// BUT, DOING AN ADDITIONAL LOG-IN WITH EITHER OF THEM WILL LOCK OUT, SINCE THAT IS THE 3RD FAILURE IN
531
		// THIS SESSION
532
533
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
534
		$member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
535
		$this->assertNotNull($member1->LockedOutUntil);
536
537
		$this->doTestLoginForm('[email protected]' , 'incorrectpassword');
538
		$member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry'));
539
		$this->assertNotNull($member2->LockedOutUntil);
540
	}
541
542
	public function testUnsuccessfulLoginAttempts() {
543
		Security::config()->login_recording = true;
0 ignored issues
show
Documentation introduced by
The property login_recording does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
544
545
		/* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */
546
		$this->doTestLoginForm('[email protected]', 'wrongpassword');
547
		$attempt = DataObject::get_one(LoginAttempt::class, array(
548
			'"LoginAttempt"."Email"' => '[email protected]'
549
		));
550
		$this->assertTrue(is_object($attempt));
551
		$member = DataObject::get_one(Member::class, array(
552
			'"Member"."Email"' => '[email protected]'
553
		));
554
		$this->assertEquals($attempt->Status, 'Failure');
555
		$this->assertEquals($attempt->Email, '[email protected]');
556
		$this->assertEquals($attempt->Member()->toMap(), $member->toMap());
557
558
		/* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
559
		$this->doTestLoginForm('[email protected]', 'wrongpassword');
560
		$attempt = DataObject::get_one(LoginAttempt::class, array(
561
			'"LoginAttempt"."Email"' => '[email protected]'
562
		));
563
		$this->assertTrue(is_object($attempt));
564
		$this->assertEquals($attempt->Status, 'Failure');
565
		$this->assertEquals($attempt->Email, '[email protected]');
566
        $this->assertNotEmpty($this->getValidationResult()->getMessages(), 'An invalid email returns a message.');
567
	}
568
569
	public function testSuccessfulLoginAttempts() {
570
		Security::config()->login_recording = true;
0 ignored issues
show
Documentation introduced by
The property login_recording does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
571
572
		/* SUCCESSFUL ATTEMPTS ARE LOGGED */
573
		$this->doTestLoginForm('[email protected]', '1nitialPassword');
574
		$attempt = DataObject::get_one(LoginAttempt::class, array(
575
			'"LoginAttempt"."Email"' => '[email protected]'
576
		));
577
		$member = DataObject::get_one(Member::class, array(
578
			'"Member"."Email"' => '[email protected]'
579
		));
580
		$this->assertTrue(is_object($attempt));
581
		$this->assertEquals($attempt->Status, 'Success');
582
		$this->assertEquals($attempt->Email, '[email protected]');
583
		$this->assertEquals($attempt->Member()->toMap(), $member->toMap());
584
	}
585
586
	public function testDatabaseIsReadyWithInsufficientMemberColumns() {
587
		$old = Security::$force_database_is_ready;
0 ignored issues
show
Documentation introduced by
The property $force_database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
588
		Security::$force_database_is_ready = null;
0 ignored issues
show
Documentation introduced by
The property $force_database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
589
		Security::$database_is_ready = false;
0 ignored issues
show
Documentation introduced by
The property $database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
590
		DBClassName::clear_classname_cache();
591
592
		// Assumption: The database has been built correctly by the test runner,
593
		// and has all columns present in the ORM
594
		/** @skipUpgrade */
595
		DB::get_schema()->renameField('Member', 'Email', 'Email_renamed');
596
597
		// Email column is now missing, which means we're not ready to do permission checks
598
		$this->assertFalse(Security::database_is_ready());
599
600
		// Rebuild the database (which re-adds the Email column), and try again
601
		$this->resetDBSchema(true);
602
		$this->assertTrue(Security::database_is_ready());
603
604
		Security::$force_database_is_ready = $old;
0 ignored issues
show
Documentation introduced by
The property $force_database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
605
	}
606
607
	/**
608
	 * Execute a log-in form using Director::test().
609
	 * Helper method for the tests above
610
	 */
611
	public function doTestLoginForm($email, $password, $backURL = 'test/link') {
612
		$this->get(Config::inst()->get(Security::class, 'logout_url'));
613
		$this->session()->inst_set('BackURL', $backURL);
614
		$this->get(Config::inst()->get(Security::class, 'login_url'));
615
616
		return $this->submitForm(
617
			"MemberLoginForm_LoginForm",
618
			null,
619
			array(
620
				'Email' => $email,
621
				'Password' => $password,
622
				'AuthenticationMethod' => MemberAuthenticator::class,
623
				'action_dologin' => 1,
624
			)
625
		);
626
	}
627
628
	/**
629
	 * Helper method to execute a change password form
630
	 */
631
	public function doTestChangepasswordForm($oldPassword, $newPassword) {
632
		return $this->submitForm(
633
			"ChangePasswordForm_ChangePasswordForm",
634
			null,
635
			array(
636
				'OldPassword' => $oldPassword,
637
				'NewPassword1' => $newPassword,
638
				'NewPassword2' => $newPassword,
639
				'action_doChangePassword' => 1,
640
			)
641
		);
642
	}
643
644
    /**
645
     * Assert this message is in the current login form errors
646
     *
647
     * @param string $expected
648
     * @param string $errorMessage
649
     */
650
	protected function assertHasMessage($expected, $errorMessage = null) {
651
        $messages = [];
652
        $result = $this->getValidationResult();
653
		if ($result) {
654
            foreach($result->getMessages() as $message) {
655
                $messages[] = $message['message'];
656
            }
657
        }
658
659
        $this->assertContains($expected, $messages, $errorMessage);
660
    }
661
662
    /**
663
     * Get validation result from last login form submission
664
     *
665
     * @return ValidationResult
666
     */
667
    protected function getValidationResult() {
668
        $result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result');
669
		if ($result) {
670
            /** @var ValidationResult $resultObj */
671
            return unserialize($result);
672
        }
673
        return null;
674
    }
675
}
676