Completed
Pull Request — master (#7026)
by Damian
08:24
created

MemberTest::tearDown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Control\Cookie;
6
use SilverStripe\Core\Convert;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\FunctionalTest;
9
use SilverStripe\i18n\i18n;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\FieldType\DBDatetime;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\Security\Group;
15
use SilverStripe\Security\IdentityStore;
16
use SilverStripe\Security\Member;
17
use SilverStripe\Security\Member_Validator;
18
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
19
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
20
use SilverStripe\Security\MemberPassword;
21
use SilverStripe\Security\PasswordEncryptor_Blowfish;
22
use SilverStripe\Security\Permission;
23
use SilverStripe\Security\RememberLoginHash;
24
use SilverStripe\Security\Security;
25
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
26
27
class MemberTest extends FunctionalTest
28
{
29
    protected static $fixture_file = 'MemberTest.yml';
30
31
    protected $orig = array();
32
33
    protected static $illegal_extensions = [
34
        Member::class => '*',
35
    ];
36
37
    public function __construct()
38
    {
39
        parent::__construct();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SilverStripe\Dev\FunctionalTest as the method __construct() does only exist in the following sub-classes of SilverStripe\Dev\FunctionalTest: SilverStripe\Security\Tests\MemberTest. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
40
41
        //Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
42
        //This is because the test relies on the yaml file being interpreted according to a particular date format
43
        //and this setup occurs before the setUp method is run
44
        i18n::config()->set('default_locale', 'en_US');
45
    }
46
47
    /**
48
     * @skipUpgrade
49
     */
50
    protected function setUp()
51
    {
52
        parent::setUp();
53
54
        Member::config()->set('unique_identifier_field', 'Email');
55
        Member::set_password_validator(null);
56
    }
57
58
    public function testWriteDoesntMergeNewRecordWithExistingMember()
59
    {
60
        $this->expectException(ValidationException::class);
61
        $m1 = new Member();
62
        $m1->Email = '[email protected]';
63
        $m1->write();
64
65
        $m2 = new Member();
66
        $m2->Email = '[email protected]';
67
        $m2->write();
68
    }
69
70
    /**
71
     * @expectedException \SilverStripe\ORM\ValidationException
72
     */
73
    public function testWriteDoesntMergeExistingMemberOnIdentifierChange()
74
    {
75
        $m1 = new Member();
76
        $m1->Email = '[email protected]';
77
        $m1->write();
78
79
        $m2 = new Member();
80
        $m2->Email = '[email protected]';
81
        $m2->write();
82
83
        $m2->Email = '[email protected]';
84
        $m2->write();
85
    }
86
87
    public function testDefaultPasswordEncryptionOnMember()
88
    {
89
        $memberWithPassword = new Member();
90
        $memberWithPassword->Password = 'mypassword';
91
        $memberWithPassword->write();
92
        $this->assertEquals(
93
            $memberWithPassword->PasswordEncryption,
94
            Security::config()->get('password_encryption_algorithm'),
95
            'Password encryption is set for new member records on first write (with setting "Password")'
96
        );
97
98
        $memberNoPassword = new Member();
99
        $memberNoPassword->write();
100
        $this->assertNull(
101
            $memberNoPassword->PasswordEncryption,
102
            'Password encryption is not set for new member records on first write, when not setting a "Password")'
103
        );
104
    }
105
106
    public function testDefaultPasswordEncryptionDoesntChangeExistingMembers()
107
    {
108
        $member = new Member();
109
        $member->Password = 'mypassword';
110
        $member->PasswordEncryption = 'sha1_v2.4';
111
        $member->write();
112
113
        Security::config()->set('password_encryption_algorithm', 'none');
114
115
        $member->Password = 'mynewpassword';
116
        $member->write();
117
118
        $this->assertEquals(
119
            $member->PasswordEncryption,
120
            'sha1_v2.4'
121
        );
122
        $auth = new MemberAuthenticator();
123
        $result = $auth->checkPassword($member, 'mynewpassword');
124
        $this->assertTrue($result->isValid());
125
    }
126
127
    public function testKeepsEncryptionOnEmptyPasswords()
128
    {
129
        $member = new Member();
130
        $member->Password = 'mypassword';
131
        $member->PasswordEncryption = 'sha1_v2.4';
132
        $member->write();
133
134
        $member->Password = '';
135
        $member->write();
136
137
        $this->assertEquals(
138
            $member->PasswordEncryption,
139
            'sha1_v2.4'
140
        );
141
        $auth = new MemberAuthenticator();
142
        $result = $auth->checkPassword($member, '');
143
        $this->assertTrue($result->isValid());
144
    }
145
146
    public function testSetPassword()
147
    {
148
        /** @var Member $member */
149
        $member = $this->objFromFixture(Member::class, 'test');
150
        $member->Password = "test1";
151
        $member->write();
152
        $auth = new MemberAuthenticator();
153
        $result = $auth->checkPassword($member, 'test1');
154
        $this->assertTrue($result->isValid());
155
    }
156
157
    /**
158
     * Test that password changes are logged properly
159
     */
160
    public function testPasswordChangeLogging()
161
    {
162
        /** @var Member $member */
163
        $member = $this->objFromFixture(Member::class, 'test');
164
        $this->assertNotNull($member);
165
        $member->Password = "test1";
166
        $member->write();
167
168
        $member->Password = "test2";
169
        $member->write();
170
171
        $member->Password = "test3";
172
        $member->write();
173
174
        $passwords = DataObject::get(MemberPassword::class, "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
175
            ->getIterator();
176
        $this->assertNotNull($passwords);
177
        $passwords->rewind();
178
        $this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
179
180
        $passwords->next();
181
        $this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
182
183
        $passwords->next();
184
        $this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
185
186
        $passwords->next();
187
        $this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
188
        $this->assertTrue(
189
            $passwords->current()->checkPassword('1nitialPassword'),
190
            "Password 1nitialPassword not found in MemberRecord"
191
        );
192
193
        //check we don't retain orphaned records when a member is deleted
194
        $member->delete();
195
196
        $passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
197
198
        $this->assertCount(0, $passwords);
199
    }
200
201
    /**
202
     * Test that changed passwords will send an email
203
     */
204
    public function testChangedPasswordEmaling()
205
    {
206
        Member::config()->update('notify_password_change', true);
207
208
        $this->clearEmails();
209
210
        /** @var Member $member */
211
        $member = $this->objFromFixture(Member::class, 'test');
212
        $this->assertNotNull($member);
213
        $valid = $member->changePassword('32asDF##$$%%');
214
        $this->assertTrue($valid->isValid());
215
216
        $this->assertEmailSent(
217
            '[email protected]',
218
            null,
219
            'Your password has been changed',
220
            '/testuser@example\.com/'
221
        );
222
    }
223
224
    /**
225
     * Test that triggering "forgotPassword" sends an Email with a reset link
226
        */
227
    public function testForgotPasswordEmaling()
228
    {
229
        $this->clearEmails();
230
        $this->autoFollowRedirection = false;
231
232
        /** @var Member $member */
233
        $member = $this->objFromFixture(Member::class, 'test');
234
        $this->assertNotNull($member);
235
236
        // Initiate a password-reset
237
        $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => $member->Email));
238
239
        $this->assertEquals($response->getStatusCode(), 302);
240
241
        // We should get redirected to Security/passwordsent
242
        $this->assertContains(
243
            'Security/lostpassword/passwordsent/[email protected]',
244
            urldecode($response->getHeader('Location'))
245
        );
246
247
        // Check existance of reset link
248
        $this->assertEmailSent(
249
            "[email protected]",
250
            null,
251
            'Your password reset link',
252
            '/Security\/changepassword\?m='.$member->ID.'&amp;t=[^"]+/'
253
        );
254
    }
255
256
    /**
257
     * Test that passwords validate against NZ e-government guidelines
258
     *  - don't allow the use of the last 6 passwords
259
     *  - require at least 3 of lowercase, uppercase, digits and punctuation
260
     *  - at least 7 characters long
261
     */
262
    public function testValidatePassword()
263
    {
264
        /**
265
 * @var Member $member
266
*/
267
        $member = $this->objFromFixture(Member::class, 'test');
268
        $this->assertNotNull($member);
269
270
        Member::set_password_validator(new MemberTest\TestPasswordValidator());
271
272
        // BAD PASSWORDS
273
274
        $result = $member->changePassword('shorty');
275
        $this->assertFalse($result->isValid());
276
        $this->assertArrayHasKey("TOO_SHORT", $result->getMessages());
277
278
        $result = $member->changePassword('longone');
279
        $this->assertArrayNotHasKey("TOO_SHORT", $result->getMessages());
280
        $this->assertArrayHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
281
        $this->assertFalse($result->isValid());
282
283
        $result = $member->changePassword('w1thNumb3rs');
284
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
285
        $this->assertTrue($result->isValid());
286
287
        // Clear out the MemberPassword table to ensure that the system functions properly in that situation
288
        DB::query("DELETE FROM \"MemberPassword\"");
289
290
        // GOOD PASSWORDS
291
292
        $result = $member->changePassword('withSym###Ls');
293
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
294
        $this->assertTrue($result->isValid());
295
296
        $result = $member->changePassword('withSym###Ls2');
297
        $this->assertTrue($result->isValid());
298
299
        $result = $member->changePassword('withSym###Ls3');
300
        $this->assertTrue($result->isValid());
301
302
        $result = $member->changePassword('withSym###Ls4');
303
        $this->assertTrue($result->isValid());
304
305
        $result = $member->changePassword('withSym###Ls5');
306
        $this->assertTrue($result->isValid());
307
308
        $result = $member->changePassword('withSym###Ls6');
309
        $this->assertTrue($result->isValid());
310
311
        $result = $member->changePassword('withSym###Ls7');
312
        $this->assertTrue($result->isValid());
313
314
        // CAN'T USE PASSWORDS 2-7, but I can use pasword 1
315
316
        $result = $member->changePassword('withSym###Ls2');
317
        $this->assertFalse($result->isValid());
318
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
319
320
        $result = $member->changePassword('withSym###Ls5');
321
        $this->assertFalse($result->isValid());
322
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
323
324
        $result = $member->changePassword('withSym###Ls7');
325
        $this->assertFalse($result->isValid());
326
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
327
328
        $result = $member->changePassword('withSym###Ls');
329
        $this->assertTrue($result->isValid());
330
331
        // HAVING DONE THAT, PASSWORD 2 is now available from the list
332
333
        $result = $member->changePassword('withSym###Ls2');
334
        $this->assertTrue($result->isValid());
335
336
        $result = $member->changePassword('withSym###Ls3');
337
        $this->assertTrue($result->isValid());
338
339
        $result = $member->changePassword('withSym###Ls4');
340
        $this->assertTrue($result->isValid());
341
342
        Member::set_password_validator(null);
343
    }
344
345
    /**
346
     * Test that the PasswordExpiry date is set when passwords are changed
347
     */
348
    public function testPasswordExpirySetting()
349
    {
350
        Member::config()->set('password_expiry_days', 90);
351
352
        /** @var Member $member */
353
        $member = $this->objFromFixture(Member::class, 'test');
354
        $this->assertNotNull($member);
355
        $valid = $member->changePassword("Xx?1234234");
356
        $this->assertTrue($valid->isValid());
357
358
        $expiryDate = date('Y-m-d', time() + 90*86400);
359
        $this->assertEquals($expiryDate, $member->PasswordExpiry);
360
361
        Member::config()->set('password_expiry_days', null);
362
        $valid = $member->changePassword("Xx?1234235");
363
        $this->assertTrue($valid->isValid());
364
365
        $this->assertNull($member->PasswordExpiry);
366
    }
367
368
    public function testIsPasswordExpired()
369
    {
370
        /** @var Member $member */
371
        $member = $this->objFromFixture(Member::class, 'test');
372
        $this->assertNotNull($member);
373
        $this->assertFalse($member->isPasswordExpired());
374
375
        $member = $this->objFromFixture(Member::class, 'noexpiry');
376
        $member->PasswordExpiry = null;
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
377
        $this->assertFalse($member->isPasswordExpired());
378
379
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
380
        $this->assertTrue($member->isPasswordExpired());
381
382
        // Check the boundary conditions
383
        // If PasswordExpiry == today, then it's expired
384
        $member->PasswordExpiry = date('Y-m-d');
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
385
        $this->assertTrue($member->isPasswordExpired());
386
387
        // If PasswordExpiry == tomorrow, then it's not
388
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
0 ignored issues
show
Documentation introduced by
The property PasswordExpiry 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...
389
        $this->assertFalse($member->isPasswordExpired());
390
    }
391
    public function testInGroups()
392
    {
393
        /** @var Member $staffmember */
394
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
395
        /** @var Member $ceomember */
396
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
397
398
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
399
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
400
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
401
402
        $this->assertTrue(
403
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
404
            'inGroups() succeeds if a membership is detected on one of many passed groups'
405
        );
406
        $this->assertFalse(
407
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
408
            'inGroups() fails if a membership is detected on none of the passed groups'
409
        );
410
        $this->assertFalse(
411
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
412
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
413
        );
414
    }
415
416
    public function testAddToGroupByCode()
417
    {
418
        /** @var Member $grouplessMember */
419
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
420
        /** @var Group $memberlessGroup */
421
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
422
423
        $this->assertFalse($grouplessMember->Groups()->exists());
424
        $this->assertFalse($memberlessGroup->Members()->exists());
425
426
        $grouplessMember->addToGroupByCode('memberless');
427
428
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
429
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
430
431
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
432
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
433
434
        /** @var Group $group */
435
        $group = DataObject::get_one(
436
            Group::class,
437
            array(
438
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
439
            )
440
        );
441
        $this->assertNotNull($group);
442
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
443
        $this->assertEquals($group->Title, 'New Group');
444
    }
445
446
    public function testRemoveFromGroupByCode()
447
    {
448
        /** @var Member $grouplessMember */
449
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
450
        /** @var Group $memberlessGroup */
451
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
452
453
        $this->assertFalse($grouplessMember->Groups()->exists());
454
        $this->assertFalse($memberlessGroup->Members()->exists());
455
456
        $grouplessMember->addToGroupByCode('memberless');
457
458
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
459
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
460
461
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
462
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
463
464
        /** @var Group $group */
465
        $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
466
        $this->assertNotNull($group);
467
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
468
        $this->assertEquals($group->Title, 'New Group');
469
470
        $grouplessMember->removeFromGroupByCode('memberless');
471
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
472
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
473
474
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
475
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
476
    }
477
478
    public function testInGroup()
479
    {
480
        /** @var Member $staffmember */
481
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
482
        /** @var Member $managementmember */
483
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
484
        /** @var Member $accountingmember */
485
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
486
        /** @var Member $ceomember */
487
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
488
489
        /** @var Group $staffgroup */
490
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
491
        /** @var Group $managementgroup */
492
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
493
        /** @var Group $ceogroup */
494
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
495
496
        $this->assertTrue(
497
            $staffmember->inGroup($staffgroup),
498
            'Direct group membership is detected'
499
        );
500
        $this->assertTrue(
501
            $managementmember->inGroup($staffgroup),
502
            'Users of child group are members of a direct parent group (if not in strict mode)'
503
        );
504
        $this->assertTrue(
505
            $accountingmember->inGroup($staffgroup),
506
            'Users of child group are members of a direct parent group (if not in strict mode)'
507
        );
508
        $this->assertTrue(
509
            $ceomember->inGroup($staffgroup),
510
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
511
        );
512
        $this->assertTrue(
513
            $ceomember->inGroup($ceogroup, true),
514
            'Direct group membership is dected (if in strict mode)'
515
        );
516
        $this->assertFalse(
517
            $ceomember->inGroup($staffgroup, true),
518
            'Users of child group are not members of a direct parent group (if in strict mode)'
519
        );
520
        $this->assertFalse(
521
            $staffmember->inGroup($managementgroup),
522
            'Users of parent group are not members of a direct child group'
523
        );
524
        $this->assertFalse(
525
            $staffmember->inGroup($ceogroup),
526
            'Users of parent group are not members of an indirect grandchild group'
527
        );
528
        $this->assertFalse(
529
            $accountingmember->inGroup($managementgroup),
530
            'Users of group are not members of any siblings'
531
        );
532
        $this->assertFalse(
533
            $staffmember->inGroup('does-not-exist'),
534
            'Non-existant group returns false'
535
        );
536
    }
537
538
    /**
539
     * Tests that the user is able to view their own record, and in turn, they can
540
     * edit and delete their own record too.
541
     */
542
    public function testCanManipulateOwnRecord()
543
    {
544
        $member = $this->objFromFixture(Member::class, 'test');
545
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
546
547
        /* Not logged in, you can't view, delete or edit the record */
548
        $this->assertFalse($member->canView());
549
        $this->assertFalse($member->canDelete());
550
        $this->assertFalse($member->canEdit());
551
552
        /* Logged in users can edit their own record */
553
        $this->logInAs($member);
554
        $this->assertTrue($member->canView());
555
        $this->assertFalse($member->canDelete());
556
        $this->assertTrue($member->canEdit());
557
558
        /* Other uses cannot view, delete or edit others records */
559
        $this->logInAs($member2);
560
        $this->assertFalse($member->canView());
561
        $this->assertFalse($member->canDelete());
562
        $this->assertFalse($member->canEdit());
563
564
        $this->logOut();
565
    }
566
567
    public function testAuthorisedMembersCanManipulateOthersRecords()
568
    {
569
        $member = $this->objFromFixture(Member::class, 'test');
570
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
571
572
        /* Group members with SecurityAdmin permissions can manipulate other records */
573
        $this->logInAs($member);
574
        $this->assertTrue($member2->canView());
575
        $this->assertTrue($member2->canDelete());
576
        $this->assertTrue($member2->canEdit());
577
578
        $this->logOut();
579
    }
580
581
    public function testExtendedCan()
582
    {
583
        $member = $this->objFromFixture(Member::class, 'test');
584
585
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
586
        $this->assertFalse($member->canView());
587
        $this->assertFalse($member->canDelete());
588
        $this->assertFalse($member->canEdit());
589
590
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
591
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
592
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
593
594
        $this->assertTrue($member2->canView());
595
        $this->assertFalse($member2->canDelete());
596
        $this->assertFalse($member2->canEdit());
597
598
        /* Apply a extension that denies viewing of the Member */
599
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
600
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
601
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
602
603
        $this->assertFalse($member3->canView());
604
        $this->assertFalse($member3->canDelete());
605
        $this->assertFalse($member3->canEdit());
606
607
        /* Apply a extension that allows viewing and editing but denies deletion */
608
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
609
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
610
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
611
612
        $this->assertTrue($member4->canView());
613
        $this->assertFalse($member4->canDelete());
614
        $this->assertTrue($member4->canEdit());
615
616
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
617
    }
618
619
    /**
620
     * Tests for {@link Member::getName()} and {@link Member::setName()}
621
     */
622
    public function testName()
623
    {
624
        /** @var Member $member */
625
        $member = $this->objFromFixture(Member::class, 'test');
626
        $member->setName('Test Some User');
627
        $this->assertEquals('Test Some User', $member->getName());
628
        $member->setName('Test');
629
        $this->assertEquals('Test', $member->getName());
630
        $member->FirstName = 'Test';
631
        $member->Surname = '';
632
        $this->assertEquals('Test', $member->getName());
633
    }
634
635
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
636
    {
637
        $adminMember = $this->objFromFixture(Member::class, 'admin');
638
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
639
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
640
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
641
642
        // Careful: Don't read as english language.
643
        // More precisely this should read canBeEditedBy()
644
645
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 637 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
646
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 637 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
647
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
0 ignored issues
show
Bug introduced by
It seems like $adminMember defined by $this->objFromFixture(\S...Member::class, 'admin') on line 637 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
648
649
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 639 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
650
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 639 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
651
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
0 ignored issues
show
Bug introduced by
It seems like $securityAdminMember defined by $this->objFromFixture(\S...\Member::class, 'test') on line 639 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
652
    }
653
654
    public function testOnChangeGroups()
655
    {
656
        /** @var Group $staffGroup */
657
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
658
        /** @var Member $staffMember */
659
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
660
        /** @var Member $adminMember */
661
        $adminMember = $this->objFromFixture(Member::class, 'admin');
662
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
663
        $newAdminGroup->write();
664
        Permission::grant($newAdminGroup->ID, 'ADMIN');
665
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
666
        $newOtherGroup->write();
667
668
        $this->assertTrue(
669
            $staffMember->onChangeGroups(array($staffGroup->ID)),
670
            'Adding existing non-admin group relation is allowed for non-admin members'
671
        );
672
        $this->assertTrue(
673
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
674
            'Adding new non-admin group relation is allowed for non-admin members'
675
        );
676
        $this->assertFalse(
677
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
678
            'Adding new admin group relation is not allowed for non-admin members'
679
        );
680
681
        $this->logInAs($adminMember);
682
        $this->assertTrue(
683
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
684
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
685
        );
686
        $this->logOut();
687
688
        $this->assertTrue(
689
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
690
            'Adding new admin group relation is allowed for admin members'
691
        );
692
    }
693
694
    /**
695
     * Test Member_GroupSet::add
696
     */
697
    public function testOnChangeGroupsByAdd()
698
    {
699
        /** @var Member $staffMember */
700
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
701
        /** @var Member $adminMember */
702
        $adminMember = $this->objFromFixture(Member::class, 'admin');
703
704
        // Setup new admin group
705
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
706
        $newAdminGroup->write();
707
        Permission::grant($newAdminGroup->ID, 'ADMIN');
708
709
        // Setup non-admin group
710
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
711
        $newOtherGroup->write();
712
713
        // Test staff can be added to other group
714
        $this->assertFalse($staffMember->inGroup($newOtherGroup));
715
        $staffMember->Groups()->add($newOtherGroup);
716
        $this->assertTrue(
717
            $staffMember->inGroup($newOtherGroup),
718
            'Adding new non-admin group relation is allowed for non-admin members'
719
        );
720
721
        // Test staff member can't be added to admin groups
722
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
723
        $staffMember->Groups()->add($newAdminGroup);
724
        $this->assertFalse(
725
            $staffMember->inGroup($newAdminGroup),
726
            'Adding new admin group relation is not allowed for non-admin members'
727
        );
728
729
        // Test staff member can be added to admin group by admins
730
        $this->logInAs($adminMember);
731
        $staffMember->Groups()->add($newAdminGroup);
732
        $this->assertTrue(
733
            $staffMember->inGroup($newAdminGroup),
734
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
735
        );
736
737
        // Test staff member can be added if they are already admin
738
        $this->logOut();
739
        $this->assertFalse($adminMember->inGroup($newAdminGroup));
740
        $adminMember->Groups()->add($newAdminGroup);
741
        $this->assertTrue(
742
            $adminMember->inGroup($newAdminGroup),
743
            'Adding new admin group relation is allowed for admin members'
744
        );
745
    }
746
747
    /**
748
     * Test Member_GroupSet::add
749
     */
750
    public function testOnChangeGroupsBySetIDList()
751
    {
752
        /** @var Member $staffMember */
753
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
754
755
        // Setup new admin group
756
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
757
        $newAdminGroup->write();
758
        Permission::grant($newAdminGroup->ID, 'ADMIN');
759
760
        // Test staff member can't be added to admin groups
761
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
762
        $staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
763
        $this->assertFalse(
764
            $staffMember->inGroup($newAdminGroup),
765
            'Adding new admin group relation is not allowed for non-admin members'
766
        );
767
    }
768
769
    /**
770
     * Test that extensions using updateCMSFields() are applied correctly
771
     */
772
    public function testUpdateCMSFields()
773
    {
774
        Member::add_extension(FieldsExtension::class);
775
776
        $member = Member::singleton();
777
        $fields = $member->getCMSFields();
778
779
        /**
780
 * @skipUpgrade
781
*/
782
        $this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
783
        $this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
784
        $this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
785
786
        Member::remove_extension(FieldsExtension::class);
787
    }
788
789
    /**
790
     * Test that all members are returned
791
     */
792
    public function testMap_in_groupsReturnsAll()
793
    {
794
        $members = Member::map_in_groups();
795
        $this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
796
    }
797
798
    /**
799
     * Test that only admin members are returned
800
     */
801
    public function testMap_in_groupsReturnsAdmins()
802
    {
803
        $adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
804
        $members = Member::map_in_groups($adminID)->toArray();
805
806
        $admin = $this->objFromFixture(Member::class, 'admin');
807
        $otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
808
809
        $this->assertTrue(
810
            in_array($admin->getTitle(), $members),
811
            $admin->getTitle().' should be in the returned list.'
812
        );
813
        $this->assertTrue(
814
            in_array($otherAdmin->getTitle(), $members),
815
            $otherAdmin->getTitle().' should be in the returned list.'
816
        );
817
        $this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
818
    }
819
820
    /**
821
     * Add the given array of member extensions as class names.
822
     * This is useful for re-adding extensions after being removed
823
     * in a test case to produce an unbiased test.
824
     *
825
     * @param  array $extensions
826
     * @return array The added extensions
827
     */
828
    protected function addExtensions($extensions)
829
    {
830
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
831
            foreach ($extensions as $extension) {
832
                Member::add_extension($extension);
833
            }
834
        }
835
        return $extensions;
836
    }
837
838
    /**
839
     * Remove given extensions from Member. This is useful for
840
     * removing extensions that could produce a biased
841
     * test result, as some extensions applied by project
842
     * code or modules can do this.
843
     *
844
     * @param  array $extensions
845
     * @return array The removed extensions
846
     */
847
    protected function removeExtensions($extensions)
848
    {
849
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
850
            foreach ($extensions as $extension) {
851
                Member::remove_extension($extension);
852
            }
853
        }
854
        return $extensions;
855
    }
856
857
    public function testGenerateAutologinTokenAndStoreHash()
858
    {
859
        $enc = new PasswordEncryptor_Blowfish();
860
861
        $m = new Member();
862
        $m->PasswordEncryption = 'blowfish';
863
        $m->Salt = $enc->salt('123');
864
865
        $token = $m->generateAutologinTokenAndStoreHash();
866
867
        $this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
868
    }
869
870
    public function testValidateAutoLoginToken()
871
    {
872
        $enc = new PasswordEncryptor_Blowfish();
873
874
        $m1 = new Member();
875
        $m1->PasswordEncryption = 'blowfish';
876
        $m1->Salt = $enc->salt('123');
877
        $m1Token = $m1->generateAutologinTokenAndStoreHash();
878
879
        $m2 = new Member();
880
        $m2->PasswordEncryption = 'blowfish';
881
        $m2->Salt = $enc->salt('456');
882
        $m2->generateAutologinTokenAndStoreHash();
883
884
        $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
885
        $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
886
    }
887
888
    public function testRememberMeHashGeneration()
889
    {
890
        /** @var Member $m1 */
891
        $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
892
893
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
894
895
        $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
896
        $this->assertEquals($hashes->count(), 1);
897
        /** @var RememberLoginHash $firstHash */
898
        $firstHash = $hashes->first();
899
        $this->assertNotNull($firstHash->DeviceID);
900
        $this->assertNotNull($firstHash->Hash);
901
    }
902
903
    public function testRememberMeHashAutologin()
904
    {
905
        /**
906
 * @var Member $m1
907
*/
908
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
909
910
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
911
912
        /** @var RememberLoginHash $firstHash */
913
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
914
        $this->assertNotNull($firstHash);
915
916
        // re-generates the hash so we can get the token
917
        $firstHash->Hash = $firstHash->getNewHash($m1);
918
        $token = $firstHash->getToken();
919
        $firstHash->write();
920
921
        $response = $this->get(
922
            'Security/login',
923
            $this->session(),
924
            null,
925
            array(
926
                'alc_enc' => $m1->ID.':'.$token,
927
                'alc_device' => $firstHash->DeviceID
928
            )
929
        );
930
        $message = Convert::raw2xml(
931
            _t(
932
                'SilverStripe\\Security\\Member.LOGGEDINAS',
933
                "You're logged in as {name}.",
934
                array('name' => $m1->FirstName)
935
            )
936
        );
937
        $this->assertContains($message, $response->getBody());
938
939
        $this->logOut();
940
941
        // A wrong token or a wrong device ID should not let us autologin
942
        $response = $this->get(
943
            'Security/login',
944
            $this->session(),
945
            null,
946
            array(
947
                'alc_enc' => $m1->ID.':asdfasd'.str_rot13($token),
948
                'alc_device' => $firstHash->DeviceID
949
            )
950
        );
951
        $this->assertNotContains($message, $response->getBody());
952
953
        $response = $this->get(
954
            'Security/login',
955
            $this->session(),
956
            null,
957
            array(
958
                'alc_enc' => $m1->ID.':'.$token,
959
                'alc_device' => str_rot13($firstHash->DeviceID)
960
            )
961
        );
962
        $this->assertNotContains($message, $response->getBody());
963
964
        // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
965
        // should remove all previous hashes for this device
966
        $response = $this->post(
967
            'Security/login/default/LoginForm',
968
            array(
969
                'Email' => $m1->Email,
970
                'Password' => '1nitialPassword',
971
                'action_doLogin' => 'action_doLogin'
972
            ),
973
            null,
974
            $this->session(),
975
            null,
976
            array(
977
                'alc_device' => $firstHash->DeviceID
978
            )
979
        );
980
        $this->assertContains($message, $response->getBody());
981
        $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
982
    }
983
984
    public function testExpiredRememberMeHashAutologin()
985
    {
986
        /** @var Member $m1 */
987
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
988
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
989
        /** @var RememberLoginHash $firstHash */
990
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
991
        $this->assertNotNull($firstHash);
992
993
        // re-generates the hash so we can get the token
994
        $firstHash->Hash = $firstHash->getNewHash($m1);
995
        $token = $firstHash->getToken();
996
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
997
        $firstHash->write();
998
999
        DBDatetime::set_mock_now('1999-12-31 23:59:59');
1000
1001
        $response = $this->get(
1002
            'Security/login',
1003
            $this->session(),
1004
            null,
1005
            array(
1006
                'alc_enc' => $m1->ID.':'.$token,
1007
                'alc_device' => $firstHash->DeviceID
1008
            )
1009
        );
1010
        $message = Convert::raw2xml(
1011
            _t(
1012
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1013
                "You're logged in as {name}.",
1014
                array('name' => $m1->FirstName)
1015
            )
1016
        );
1017
        $this->assertContains($message, $response->getBody());
1018
1019
        $this->logOut();
1020
1021
        // re-generates the hash so we can get the token
1022
        $firstHash->Hash = $firstHash->getNewHash($m1);
1023
        $token = $firstHash->getToken();
1024
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1025
        $firstHash->write();
1026
1027
        DBDatetime::set_mock_now('2000-01-01 00:00:01');
1028
1029
        $response = $this->get(
1030
            'Security/login',
1031
            $this->session(),
1032
            null,
1033
            array(
1034
                'alc_enc' => $m1->ID.':'.$token,
1035
                'alc_device' => $firstHash->DeviceID
1036
            )
1037
        );
1038
        $this->assertNotContains($message, $response->getBody());
1039
        $this->logOut();
1040
        DBDatetime::clear_mock_now();
1041
    }
1042
1043
    public function testRememberMeMultipleDevices()
1044
    {
1045
        /** @var Member $m1 */
1046
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1047
1048
        // First device
1049
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1050
        Cookie::set('alc_device', null);
1051
        // Second device
1052
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1053
1054
        // Hash of first device
1055
        /** @var RememberLoginHash $firstHash */
1056
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1057
        $this->assertNotNull($firstHash);
1058
1059
        // Hash of second device
1060
        /** @var RememberLoginHash $secondHash */
1061
        $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
1062
        $this->assertNotNull($secondHash);
1063
1064
        // DeviceIDs are different
1065
        $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1066
1067
        // re-generates the hashes so we can get the tokens
1068
        $firstHash->Hash = $firstHash->getNewHash($m1);
1069
        $firstToken = $firstHash->getToken();
1070
        $firstHash->write();
1071
1072
        $secondHash->Hash = $secondHash->getNewHash($m1);
1073
        $secondToken = $secondHash->getToken();
1074
        $secondHash->write();
1075
1076
        // Accessing the login page should show the user's name straight away
1077
        $response = $this->get(
1078
            'Security/login',
1079
            $this->session(),
1080
            null,
1081
            array(
1082
                'alc_enc' => $m1->ID.':'.$firstToken,
1083
                'alc_device' => $firstHash->DeviceID
1084
            )
1085
        );
1086
        $message = Convert::raw2xml(
1087
            _t(
1088
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1089
                "You're logged in as {name}.",
1090
                array('name' => $m1->FirstName)
1091
            )
1092
        );
1093
        $this->assertContains($message, $response->getBody());
1094
1095
        // Test that removing session but not cookie keeps user
1096
        /** @var SessionAuthenticationHandler $sessionHandler */
1097
        $sessionHandler = Injector::inst()->get(SessionAuthenticationHandler::class);
1098
        $sessionHandler->logOut();
1099
        Security::setCurrentUser(null);
1100
1101
        // Accessing the login page from the second device
1102
        $response = $this->get(
1103
            'Security/login',
1104
            $this->session(),
1105
            null,
1106
            array(
1107
                'alc_enc' => $m1->ID.':'.$secondToken,
1108
                'alc_device' => $secondHash->DeviceID
1109
            )
1110
        );
1111
        $this->assertContains($message, $response->getBody());
1112
1113
        // Logging out from the second device - only one device being logged out
1114
        RememberLoginHash::config()->update('logout_across_devices', false);
1115
        $this->get(
1116
            'Security/logout',
1117
            $this->session(),
1118
            null,
1119
            array(
1120
                'alc_enc' => $m1->ID.':'.$secondToken,
1121
                'alc_device' => $secondHash->DeviceID
1122
            )
1123
        );
1124
        $this->assertEquals(
1125
            RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1126
            1
1127
        );
1128
1129
        // Logging out from any device when all login hashes should be removed
1130
        RememberLoginHash::config()->update('logout_across_devices', true);
1131
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1132
        $this->get('Security/logout', $this->session());
1133
        $this->assertEquals(
1134
            RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1135
            0
1136
        );
1137
    }
1138
1139
    public function testCanDelete()
1140
    {
1141
        $admin1 = $this->objFromFixture(Member::class, 'admin');
1142
        $admin2 = $this->objFromFixture(Member::class, 'other-admin');
1143
        $member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1144
        $member2 = $this->objFromFixture(Member::class, 'noformatmember');
1145
1146
        $this->assertTrue(
1147
            $admin1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1142 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1148
            'Admins can delete other admins'
1149
        );
1150
        $this->assertTrue(
1151
            $member1->canDelete($admin2),
0 ignored issues
show
Bug introduced by
It seems like $admin2 defined by $this->objFromFixture(\S...::class, 'other-admin') on line 1142 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1152
            'Admins can delete non-admins'
1153
        );
1154
        $this->assertFalse(
1155
            $admin1->canDelete($admin1),
0 ignored issues
show
Bug introduced by
It seems like $admin1 defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1141 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1156
            'Admins can not delete themselves'
1157
        );
1158
        $this->assertFalse(
1159
            $member1->canDelete($member2),
0 ignored issues
show
Bug introduced by
It seems like $member2 defined by $this->objFromFixture(\S...lass, 'noformatmember') on line 1144 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1160
            'Non-admins can not delete other non-admins'
1161
        );
1162
        $this->assertFalse(
1163
            $member1->canDelete($member1),
0 ignored issues
show
Bug introduced by
It seems like $member1 defined by $this->objFromFixture(\S...ass, 'grouplessmember') on line 1143 can also be of type object<SilverStripe\ORM\DataObject>; however, SilverStripe\ORM\DataObject::canDelete() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1164
            'Non-admins can not delete themselves'
1165
        );
1166
    }
1167
1168
    public function testFailedLoginCount()
1169
    {
1170
        $maxFailedLoginsAllowed = 3;
1171
        //set up the config variables to enable login lockouts
1172
        Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1173
1174
        /** @var Member $member */
1175
        $member = $this->objFromFixture(Member::class, 'test');
1176
        $failedLoginCount = $member->FailedLoginCount;
1177
1178
        for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1179
            $member->registerFailedLogin();
1180
1181
            $this->assertEquals(
1182
                ++$failedLoginCount,
1183
                $member->FailedLoginCount,
1184
                'Failed to increment $member->FailedLoginCount'
1185
            );
1186
1187
            $this->assertTrue(
1188
                $member->canLogin(),
1189
                "Member has been locked out too early"
1190
            );
1191
        }
1192
    }
1193
1194
    public function testMemberValidator()
1195
    {
1196
        // clear custom requirements for this test
1197
        Member_Validator::config()->update('customRequired', null);
1198
        /** @var Member $memberA */
1199
        $memberA = $this->objFromFixture(Member::class, 'admin');
1200
        /** @var Member $memberB */
1201
        $memberB = $this->objFromFixture(Member::class, 'test');
1202
1203
        // create a blank form
1204
        $form = new MemberTest\ValidatorForm();
1205
1206
        $validator = new Member_Validator();
1207
        $validator->setForm($form);
1208
1209
        // Simulate creation of a new member via form, but use an existing member identifier
1210
        $fail = $validator->php(
1211
            array(
1212
            'FirstName' => 'Test',
1213
            'Email' => $memberA->Email
1214
            )
1215
        );
1216
1217
        $this->assertFalse(
1218
            $fail,
1219
            'Member_Validator must fail when trying to create new Member with existing Email.'
1220
        );
1221
1222
        // populate the form with values from another member
1223
        $form->loadDataFrom($memberB);
1224
1225
        // Assign the validator to an existing member
1226
        // (this is basically the same as passing the member ID with the form data)
1227
        $validator->setForMember($memberB);
1228
1229
        // Simulate update of a member via form and use an existing member Email
1230
        $fail = $validator->php(
1231
            array(
1232
            'FirstName' => 'Test',
1233
            'Email' => $memberA->Email
1234
            )
1235
        );
1236
1237
        // Simulate update to a new Email address
1238
        $pass1 = $validator->php(
1239
            array(
1240
            'FirstName' => 'Test',
1241
            'Email' => '[email protected]'
1242
            )
1243
        );
1244
1245
        // Pass in the same Email address that the member already has. Ensure that case is valid
1246
        $pass2 = $validator->php(
1247
            array(
1248
            'FirstName' => 'Test',
1249
            'Surname' => 'User',
1250
            'Email' => $memberB->Email
1251
            )
1252
        );
1253
1254
        $this->assertFalse(
1255
            $fail,
1256
            'Member_Validator must fail when trying to update existing member with existing Email.'
1257
        );
1258
1259
        $this->assertTrue(
1260
            $pass1,
1261
            'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1262
        );
1263
1264
        $this->assertTrue(
1265
            $pass2,
1266
            'Member_Validator must pass when Member updates his own Email to the already existing value.'
1267
        );
1268
    }
1269
1270
    public function testMemberValidatorWithExtensions()
1271
    {
1272
        // clear custom requirements for this test
1273
        Member_Validator::config()->update('customRequired', null);
1274
1275
        // create a blank form
1276
        $form = new MemberTest\ValidatorForm();
1277
1278
        // Test extensions
1279
        Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1280
        $validator = new Member_Validator();
1281
        $validator->setForm($form);
1282
1283
        // This test should fail, since the extension enforces FirstName == Surname
1284
        $fail = $validator->php(
1285
            array(
1286
            'FirstName' => 'Test',
1287
            'Surname' => 'User',
1288
            'Email' => '[email protected]'
1289
            )
1290
        );
1291
1292
        $pass = $validator->php(
1293
            array(
1294
            'FirstName' => 'Test',
1295
            'Surname' => 'Test',
1296
            'Email' => '[email protected]'
1297
            )
1298
        );
1299
1300
        $this->assertFalse(
1301
            $fail,
1302
            'Member_Validator must fail because of added extension.'
1303
        );
1304
1305
        $this->assertTrue(
1306
            $pass,
1307
            'Member_Validator must succeed, since it meets all requirements.'
1308
        );
1309
1310
        // Add another extension that always fails. This ensures that all extensions are considered in the validation
1311
        Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1312
        $validator = new Member_Validator();
1313
        $validator->setForm($form);
1314
1315
        // Even though the data is valid, This test should still fail, since one extension always returns false
1316
        $fail = $validator->php(
1317
            array(
1318
            'FirstName' => 'Test',
1319
            'Surname' => 'Test',
1320
            'Email' => '[email protected]'
1321
            )
1322
        );
1323
1324
        $this->assertFalse(
1325
            $fail,
1326
            'Member_Validator must fail because of added extensions.'
1327
        );
1328
1329
        // Remove added extensions
1330
        Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1331
        Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1332
    }
1333
1334
    public function testCustomMemberValidator()
1335
    {
1336
        // clear custom requirements for this test
1337
        Member_Validator::config()->update('customRequired', null);
1338
1339
        $member = $this->objFromFixture(Member::class, 'admin');
1340
1341
        $form = new MemberTest\ValidatorForm();
1342
        $form->loadDataFrom($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->objFromFixture(\S...Member::class, 'admin') on line 1339 can be null; however, SilverStripe\Forms\Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1343
1344
        $validator = new Member_Validator();
1345
        $validator->setForm($form);
1346
1347
        $pass = $validator->php(
1348
            array(
1349
            'FirstName' => 'Borris',
1350
            'Email' => '[email protected]'
1351
            )
1352
        );
1353
1354
        $fail = $validator->php(
1355
            array(
1356
            'Email' => '[email protected]',
1357
            'Surname' => ''
1358
            )
1359
        );
1360
1361
        $this->assertTrue($pass, 'Validator requires a FirstName and Email');
1362
        $this->assertFalse($fail, 'Missing FirstName');
1363
1364
        $ext = new MemberTest\ValidatorExtension();
1365
        $ext->updateValidator($validator);
1366
1367
        $pass = $validator->php(
1368
            array(
1369
            'FirstName' => 'Borris',
1370
            'Email' => '[email protected]'
1371
            )
1372
        );
1373
1374
        $fail = $validator->php(
1375
            array(
1376
            'Email' => '[email protected]'
1377
            )
1378
        );
1379
1380
        $this->assertFalse($pass, 'Missing surname');
1381
        $this->assertFalse($fail, 'Missing surname value');
1382
1383
        $fail = $validator->php(
1384
            array(
1385
            'Email' => '[email protected]',
1386
            'Surname' => 'Silverman'
1387
            )
1388
        );
1389
1390
        $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1391
    }
1392
1393
    public function testCurrentUser()
1394
    {
1395
        $this->assertNull(Security::getCurrentUser());
1396
1397
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1398
        $this->logInAs($adminMember);
1399
1400
        $userFromSession = Security::getCurrentUser();
1401
        $this->assertEquals($adminMember->ID, $userFromSession->ID);
1402
    }
1403
1404
    /**
1405
     * @covers \SilverStripe\Security\Member::actAs()
1406
     */
1407
    public function testActAsUserPermissions()
1408
    {
1409
        $this->assertNull(Security::getCurrentUser());
1410
1411
        /** @var Member $adminMember */
1412
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1413
1414
        // Check acting as admin when not logged in
1415
        $checkAdmin = Member::actAs($adminMember, function () {
1416
            return Permission::check('ADMIN');
1417
        });
1418
        $this->assertTrue($checkAdmin);
1419
1420
        // Check nesting
1421
        $checkAdmin = Member::actAs($adminMember, function () {
1422
            return Member::actAs(null, function () {
1423
                return Permission::check('ADMIN');
1424
            });
1425
        });
1426
        $this->assertFalse($checkAdmin);
1427
1428
        // Check logging in as non-admin user
1429
        $this->logInWithPermission('TEST_PERMISSION');
1430
1431
        $hasPerm = Member::actAs(null, function () {
1432
            return Permission::check('TEST_PERMISSION');
1433
        });
1434
        $this->assertFalse($hasPerm);
1435
1436
        // Check permissions can be promoted
1437
        $checkAdmin = Member::actAs($adminMember, function () {
1438
            return Permission::check('ADMIN');
1439
        });
1440
        $this->assertTrue($checkAdmin);
1441
    }
1442
1443
    /**
1444
     * @covers \SilverStripe\Security\Member::actAs()
1445
     */
1446
    public function testActAsUser()
1447
    {
1448
        $this->assertNull(Security::getCurrentUser());
1449
1450
        /** @var Member $adminMember */
1451
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1452
        $member = Member::actAs($adminMember, function () {
1453
            return Security::getCurrentUser();
1454
        });
1455
        $this->assertEquals($adminMember->ID, $member->ID);
1456
1457
        // Check nesting
1458
        $member = Member::actAs($adminMember, function () {
1459
            return Member::actAs(null, function () {
1460
                return Security::getCurrentUser();
1461
            });
1462
        });
1463
        $this->assertEmpty($member);
1464
    }
1465
}
1466