Completed
Push — authenticator-refactor ( 82d769...121803 )
by Sam
17:22
created

MemberAuthenticatorTest::setUp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 9
rs 9.6666
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\ValidationResult;
8
use SilverStripe\Security\PasswordEncryptor;
9
use SilverStripe\Security\PasswordEncryptor_PHPHash;
10
use SilverStripe\Security\Security;
11
use SilverStripe\Security\Member;
12
use SilverStripe\Security\MemberAuthenticator\Authenticator;
13
use SilverStripe\Security\MemberAuthenticator\LoginForm;
14
use SilverStripe\Security\CMSMemberLoginForm;
15
use SilverStripe\Core\Config\Config;
16
use SilverStripe\Dev\SapphireTest;
17
use SilverStripe\Forms\FieldList;
18
use SilverStripe\Forms\Form;
19
use SilverStripe\Control\HTTPRequest;
20
21
class MemberAuthenticatorTest extends SapphireTest
22
{
23
24
    protected $usesDatabase = true;
25
26
    protected $defaultUsername = null;
27
    protected $defaultPassword = null;
28
29
    protected function setUp()
30
    {
31
        parent::setUp();
32
33
        $this->defaultUsername = Security::default_admin_username();
34
        $this->defaultPassword = Security::default_admin_password();
35
        Security::clear_default_admin();
36
        Security::setDefaultAdmin('admin', 'password');
37
    }
38
39
    protected function tearDown()
40
    {
41
        Security::setDefaultAdmin($this->defaultUsername, $this->defaultPassword);
42
        parent::tearDown();
43
    }
44
45
    public function testCustomIdentifierField()
46
    {
47
48
        $origField = 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...
49
        Member::config()->unique_identifier_field = 'Username';
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...
50
51
        $label=singleton(Member::class)->fieldLabel(Member::config()->unique_identifier_field);
52
53
        $this->assertEquals($label, 'Username');
54
55
        Member::config()->unique_identifier_field = $origField;
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...
56
    }
57
58
    public function testGenerateLoginForm()
59
    {
60
        $authenticator = new Authenticator();
61
62
        $controller = new Security();
63
64
        // Create basic login form
65
        $frontendResponse = $authenticator
66
            ->getLoginHandler($controller->link())
67
            ->handleRequest(new HTTPRequest('get', '/'), \SilverStripe\ORM\DataModel::inst());
68
69
        $this->assertTrue(is_array($frontendResponse));
70
        $this->assertTrue(isset($frontendResponse['Form']));
71
        $this->assertTrue($frontendResponse['Form'] instanceof LoginForm);
72
    }
73
74
    /* TO DO - reenable
75
    public function testGenerateCMSLoginForm()
76
    {
77
        $authenticator = new Authenticator();
78
79
        // Supports cms login form
80
        $this->assertTrue(MemberAuthenticator::supports_cms());
81
        $cmsForm = MemberAuthenticator::get_cms_login_form($controller);
82
        $this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
83
    }
84
    */
85
86
87
    /**
88
     * Test that a member can be authenticated via their temp id
89
     */
90
    public function testAuthenticateByTempID()
91
    {
92
        $authenticator = new Authenticator();
93
94
        $member = new Member();
95
        $member->Email = '[email protected]';
96
        $member->PasswordEncryption = "sha1";
97
        $member->Password = "mypassword";
98
        $member->write();
99
100
        // If the user has never logged in, then the tempid should be empty
101
        $tempID = $member->TempIDHash;
102
        $this->assertEmpty($tempID);
103
104
        // If the user logs in then they have a temp id
105
        $member->logIn(true);
106
        $tempID = $member->TempIDHash;
107
        $this->assertNotEmpty($tempID);
108
109
        // Test correct login
110
        $result = $authenticator->authenticate(
111
            array(
112
            'tempid' => $tempID,
113
            'Password' => 'mypassword'
114
            ),
115
            $message
116
        );
117
118
        $this->assertNotEmpty($result);
119
        $this->assertEquals($result->ID, $member->ID);
120
        $this->assertEmpty($message);
121
122
        // Test incorrect login
123
        $result = $authenticator->authenticate(
124
            array(
125
            'tempid' => $tempID,
126
            'Password' => 'notmypassword'
127
            ),
128
            $message
129
        );
130
131
        $this->assertEmpty($result);
132
        $this->assertEquals(
133
            _t('Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
134
            $message
135
        );
136
    }
137
138
    /**
139
     * Test that the default admin can be authenticated
140
     */
141
    public function testDefaultAdmin()
142
    {
143
        $authenticator = new Authenticator();
144
145
        // Test correct login
146
        $result = $authenticator->authenticate(
147
            array(
148
            'Email' => 'admin',
149
            'Password' => 'password'
150
            ),
151
            $message
152
        );
153
        $this->assertNotEmpty($result);
154
        $this->assertEquals($result->Email, Security::default_admin_username());
155
        $this->assertEmpty($message);
156
157
        // Test incorrect login
158
        $result = $authenticator->authenticate(
159
            array(
160
            'Email' => 'admin',
161
            'Password' => 'notmypassword'
162
            ),
163
            $message
164
        );
165
        $this->assertEmpty($result);
166
        $this->assertEquals(
167
            'The provided details don\'t seem to be correct. Please try again.',
168
            $message
169
        );
170
    }
171
172
    public function testDefaultAdminLockOut()
173
    {
174
        $authenticator = new Authenticator();
175
176
        Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
177
        Config::inst()->update(Member::class, 'lock_out_delay_mins', 10);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
178
        DBDatetime::set_mock_now('2016-04-18 00:00:00');
179
180
        // Test correct login
181
        $authenticator->authenticate(
182
            [
183
                'Email' => 'admin',
184
                'Password' => 'wrongpassword'
185
            ],
186
            $dummy
187
        );
188
189
        $this->assertTrue(Member::default_admin()->isLockedOut());
190
        $this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil);
191
    }
192
}
193