Passed
Push — 4.2 ( ddc869...7f685b )
by Robbie
17:08 queued 08:15
created

testNewMembersReceiveTheDefaultLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Control\Cookie;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Dev\FunctionalTest;
10
use SilverStripe\Forms\ListboxField;
11
use SilverStripe\i18n\i18n;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\ORM\FieldType\DBDatetime;
15
use SilverStripe\ORM\ValidationException;
16
use SilverStripe\ORM\ValidationResult;
17
use SilverStripe\Security\Group;
18
use SilverStripe\Security\IdentityStore;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\Member_Validator;
21
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
22
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
23
use SilverStripe\Security\MemberPassword;
24
use SilverStripe\Security\PasswordEncryptor_Blowfish;
25
use SilverStripe\Security\Permission;
26
use SilverStripe\Security\RememberLoginHash;
27
use SilverStripe\Security\Security;
28
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
29
30
class MemberTest extends FunctionalTest
31
{
32
    protected static $fixture_file = 'MemberTest.yml';
33
34
    protected $orig = array();
35
36
    protected static $illegal_extensions = [
37
        Member::class => '*',
38
    ];
39
40
    public static function setUpBeforeClass()
41
    {
42
        parent::setUpBeforeClass();
43
44
        //Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
45
        //This is because the test relies on the yaml file being interpreted according to a particular date format
46
        //and this setup occurs before the setUp method is run
47
        i18n::config()->set('default_locale', 'en_US');
48
    }
49
50
    /**
51
     * @skipUpgrade
52
     */
53
    protected function setUp()
54
    {
55
        parent::setUp();
56
57
        Member::config()->set('unique_identifier_field', 'Email');
58
        Member::set_password_validator(null);
59
60
        i18n::set_locale('en_US');
61
    }
62
63
    public function testPasswordEncryptionUpdatedOnChangedPassword()
64
    {
65
        Config::modify()->set(Security::class, 'password_encryption_algorithm', 'none');
66
        $member = Member::create();
67
        $member->Password = 'password';
68
        $member->write();
69
        $this->assertEquals('password', $member->Password);
70
        $this->assertEquals('none', $member->PasswordEncryption);
71
        Config::modify()->set(Security::class, 'password_encryption_algorithm', 'blowfish');
72
        $member->Password = 'newpassword';
73
        $member->write();
74
        $this->assertNotEquals('password', $member->Password);
75
        $this->assertNotEquals('newpassword', $member->Password);
76
        $this->assertEquals('blowfish', $member->PasswordEncryption);
77
    }
78
79
    public function testWriteDoesntMergeNewRecordWithExistingMember()
80
    {
81
        $this->expectException(ValidationException::class);
82
        $m1 = new Member();
83
        $m1->Email = '[email protected]';
84
        $m1->write();
85
86
        $m2 = new Member();
87
        $m2->Email = '[email protected]';
88
        $m2->write();
89
    }
90
91
    /**
92
     * @expectedException \SilverStripe\ORM\ValidationException
93
     */
94
    public function testWriteDoesntMergeExistingMemberOnIdentifierChange()
95
    {
96
        $m1 = new Member();
97
        $m1->Email = '[email protected]';
98
        $m1->write();
99
100
        $m2 = new Member();
101
        $m2->Email = '[email protected]';
102
        $m2->write();
103
104
        $m2->Email = '[email protected]';
105
        $m2->write();
106
    }
107
108
    public function testDefaultPasswordEncryptionOnMember()
109
    {
110
        $memberWithPassword = new Member();
111
        $memberWithPassword->Password = 'mypassword';
112
        $memberWithPassword->write();
113
        $this->assertEquals(
114
            Security::config()->get('password_encryption_algorithm'),
115
            $memberWithPassword->PasswordEncryption,
116
            'Password encryption is set for new member records on first write (with setting "Password")'
117
        );
118
119
        $memberNoPassword = new Member();
120
        $memberNoPassword->write();
121
        $this->assertEquals(
122
            Security::config()->get('password_encryption_algorithm'),
123
            $memberNoPassword->PasswordEncryption,
124
            'Password encryption is not set for new member records on first write, when not setting a "Password")'
125
        );
126
    }
127
128
    public function testKeepsEncryptionOnEmptyPasswords()
129
    {
130
        $member = new Member();
131
        $member->Password = 'mypassword';
132
        $member->PasswordEncryption = 'sha1_v2.4';
133
        $member->write();
134
135
        $member->Password = '';
136
        $member->write();
137
138
        $this->assertEquals(
139
            Security::config()->get('password_encryption_algorithm'),
140
            $member->PasswordEncryption
141
        );
142
        $auth = new MemberAuthenticator();
143
        $result = $auth->checkPassword($member, '');
144
        $this->assertTrue($result->isValid());
145
    }
146
147
    public function testSetPassword()
148
    {
149
        /** @var Member $member */
150
        $member = $this->objFromFixture(Member::class, 'test');
151
        $member->Password = "test1";
152
        $member->write();
153
        $auth = new MemberAuthenticator();
154
        $result = $auth->checkPassword($member, 'test1');
155
        $this->assertTrue($result->isValid());
156
    }
157
158
    /**
159
     * Test that password changes are logged properly
160
     */
161
    public function testPasswordChangeLogging()
162
    {
163
        /** @var Member $member */
164
        $member = $this->objFromFixture(Member::class, 'test');
165
        $this->assertNotNull($member);
166
        $member->Password = "test1";
167
        $member->write();
168
169
        $member->Password = "test2";
170
        $member->write();
171
172
        $member->Password = "test3";
173
        $member->write();
174
175
        $passwords = DataObject::get(MemberPassword::class, "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
176
            ->getIterator();
177
        $this->assertNotNull($passwords);
178
        $passwords->rewind();
179
        $this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
180
181
        $passwords->next();
182
        $this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
183
184
        $passwords->next();
185
        $this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
186
187
        $passwords->next();
188
        $this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
189
        $this->assertTrue(
190
            $passwords->current()->checkPassword('1nitialPassword'),
191
            "Password 1nitialPassword not found in MemberRecord"
192
        );
193
194
        //check we don't retain orphaned records when a member is deleted
195
        $member->delete();
196
197
        $passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
198
199
        $this->assertCount(0, $passwords);
200
    }
201
202
    /**
203
     * Test that changed passwords will send an email
204
     */
205
    public function testChangedPasswordEmaling()
206
    {
207
        Member::config()->update('notify_password_change', true);
208
209
        $this->clearEmails();
210
211
        /** @var Member $member */
212
        $member = $this->objFromFixture(Member::class, 'test');
213
        $this->assertNotNull($member);
214
        $valid = $member->changePassword('32asDF##$$%%');
215
        $this->assertTrue($valid->isValid());
216
217
        $this->assertEmailSent(
218
            '[email protected]',
219
            null,
220
            'Your password has been changed',
221
            '/testuser@example\.com/'
222
        );
223
    }
224
225
    /**
226
     * Test that triggering "forgotPassword" sends an Email with a reset link
227
        */
228
    public function testForgotPasswordEmaling()
229
    {
230
        $this->clearEmails();
231
        $this->autoFollowRedirection = false;
232
233
        /** @var Member $member */
234
        $member = $this->objFromFixture(Member::class, 'test');
235
        $this->assertNotNull($member);
236
237
        // Initiate a password-reset
238
        $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => $member->Email));
239
240
        $this->assertEquals($response->getStatusCode(), 302);
241
242
        // We should get redirected to Security/passwordsent
243
        $this->assertContains(
244
            'Security/lostpassword/passwordsent',
245
            urldecode($response->getHeader('Location'))
246
        );
247
248
        // Check existance of reset link
249
        $this->assertEmailSent(
250
            "[email protected]",
251
            null,
252
            'Your password reset link',
253
            '/Security\/changepassword\?m=' . $member->ID . '&amp;t=[^"]+/'
254
        );
255
    }
256
257
    /**
258
     * Test that passwords validate against NZ e-government guidelines
259
     *  - don't allow the use of the last 6 passwords
260
     *  - require at least 3 of lowercase, uppercase, digits and punctuation
261
     *  - at least 7 characters long
262
     */
263
    public function testValidatePassword()
264
    {
265
        /**
266
         * @var Member $member
267
        */
268
        $member = $this->objFromFixture(Member::class, 'test');
269
        $this->assertNotNull($member);
270
271
        Member::set_password_validator(new MemberTest\TestPasswordValidator());
272
273
        // BAD PASSWORDS
274
275
        $result = $member->changePassword('shorty');
276
        $this->assertFalse($result->isValid());
277
        $this->assertArrayHasKey("TOO_SHORT", $result->getMessages());
278
279
        $result = $member->changePassword('longone');
280
        $this->assertArrayNotHasKey("TOO_SHORT", $result->getMessages());
281
        $this->assertArrayHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
282
        $this->assertFalse($result->isValid());
283
284
        $result = $member->changePassword('w1thNumb3rs');
285
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
286
        $this->assertTrue($result->isValid());
287
288
        // Clear out the MemberPassword table to ensure that the system functions properly in that situation
289
        DB::query("DELETE FROM \"MemberPassword\"");
290
291
        // GOOD PASSWORDS
292
293
        $result = $member->changePassword('withSym###Ls');
294
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
295
        $this->assertTrue($result->isValid());
296
297
        $result = $member->changePassword('withSym###Ls2');
298
        $this->assertTrue($result->isValid());
299
300
        $result = $member->changePassword('withSym###Ls3');
301
        $this->assertTrue($result->isValid());
302
303
        $result = $member->changePassword('withSym###Ls4');
304
        $this->assertTrue($result->isValid());
305
306
        $result = $member->changePassword('withSym###Ls5');
307
        $this->assertTrue($result->isValid());
308
309
        $result = $member->changePassword('withSym###Ls6');
310
        $this->assertTrue($result->isValid());
311
312
        $result = $member->changePassword('withSym###Ls7');
313
        $this->assertTrue($result->isValid());
314
315
        // CAN'T USE PASSWORDS 2-7, but I can use pasword 1
316
317
        $result = $member->changePassword('withSym###Ls2');
318
        $this->assertFalse($result->isValid());
319
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
320
321
        $result = $member->changePassword('withSym###Ls5');
322
        $this->assertFalse($result->isValid());
323
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
324
325
        $result = $member->changePassword('withSym###Ls7');
326
        $this->assertFalse($result->isValid());
327
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
328
329
        $result = $member->changePassword('withSym###Ls');
330
        $this->assertTrue($result->isValid());
331
332
        // HAVING DONE THAT, PASSWORD 2 is now available from the list
333
334
        $result = $member->changePassword('withSym###Ls2');
335
        $this->assertTrue($result->isValid());
336
337
        $result = $member->changePassword('withSym###Ls3');
338
        $this->assertTrue($result->isValid());
339
340
        $result = $member->changePassword('withSym###Ls4');
341
        $this->assertTrue($result->isValid());
342
343
        Member::set_password_validator(null);
344
    }
345
346
    /**
347
     * Test that the PasswordExpiry date is set when passwords are changed
348
     */
349
    public function testPasswordExpirySetting()
350
    {
351
        Member::config()->set('password_expiry_days', 90);
352
353
        /** @var Member $member */
354
        $member = $this->objFromFixture(Member::class, 'test');
355
        $this->assertNotNull($member);
356
        $valid = $member->changePassword("Xx?1234234");
357
        $this->assertTrue($valid->isValid());
358
359
        $expiryDate = date('Y-m-d', time() + 90*86400);
360
        $this->assertEquals($expiryDate, $member->PasswordExpiry);
361
362
        Member::config()->set('password_expiry_days', null);
363
        $valid = $member->changePassword("Xx?1234235");
364
        $this->assertTrue($valid->isValid());
365
366
        $this->assertNull($member->PasswordExpiry);
367
    }
368
369
    public function testIsPasswordExpired()
370
    {
371
        /** @var Member $member */
372
        $member = $this->objFromFixture(Member::class, 'test');
373
        $this->assertNotNull($member);
374
        $this->assertFalse($member->isPasswordExpired());
375
376
        $member = $this->objFromFixture(Member::class, 'noexpiry');
377
        $member->PasswordExpiry = null;
0 ignored issues
show
Bug Best Practice introduced by
The property PasswordExpiry does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
378
        $this->assertFalse($member->isPasswordExpired());
0 ignored issues
show
Bug introduced by
The method isPasswordExpired() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

378
        $this->assertFalse($member->/** @scrutinizer ignore-call */ isPasswordExpired());
Loading history...
379
380
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
381
        $this->assertTrue($member->isPasswordExpired());
382
383
        // Check the boundary conditions
384
        // If PasswordExpiry == today, then it's expired
385
        $member->PasswordExpiry = date('Y-m-d');
386
        $this->assertTrue($member->isPasswordExpired());
387
388
        // If PasswordExpiry == tomorrow, then it's not
389
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
390
        $this->assertFalse($member->isPasswordExpired());
391
    }
392
    public function testInGroups()
393
    {
394
        /** @var Member $staffmember */
395
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
396
        /** @var Member $ceomember */
397
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
398
399
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
400
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
401
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
402
403
        $this->assertTrue(
404
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
405
            'inGroups() succeeds if a membership is detected on one of many passed groups'
406
        );
407
        $this->assertFalse(
408
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
409
            'inGroups() fails if a membership is detected on none of the passed groups'
410
        );
411
        $this->assertFalse(
412
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
413
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
414
        );
415
    }
416
417
    /**
418
     * Assertions to check that Member_GroupSet is functionally equivalent to ManyManyList
419
     */
420
    public function testRemoveGroups()
421
    {
422
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
423
424
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
425
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
426
427
        $this->assertTrue(
428
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
0 ignored issues
show
Bug introduced by
The method inGroups() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

428
            $staffmember->/** @scrutinizer ignore-call */ 
429
                          inGroups(array($staffgroup, $managementgroup)),
Loading history...
429
            'inGroups() succeeds if a membership is detected on one of many passed groups'
430
        );
431
432
        $staffmember->Groups()->remove($managementgroup);
0 ignored issues
show
Bug introduced by
The method Groups() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

432
        $staffmember->/** @scrutinizer ignore-call */ 
433
                      Groups()->remove($managementgroup);
Loading history...
433
        $this->assertFalse(
434
            $staffmember->inGroup($managementgroup),
0 ignored issues
show
Bug introduced by
The method inGroup() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

434
            $staffmember->/** @scrutinizer ignore-call */ 
435
                          inGroup($managementgroup),
Loading history...
435
            'member was not removed from group using ->Groups()->remove()'
436
        );
437
438
        $staffmember->Groups()->removeAll();
439
        $this->assertCount(
440
            0,
441
            $staffmember->Groups(),
442
            'member was not removed from all groups using ->Groups()->removeAll()'
443
        );
444
    }
445
446
    public function testAddToGroupByCode()
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(
466
            Group::class,
467
            array(
468
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
469
            )
470
        );
471
        $this->assertNotNull($group);
472
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
473
        $this->assertEquals($group->Title, 'New Group');
474
    }
475
476
    public function testRemoveFromGroupByCode()
477
    {
478
        /** @var Member $grouplessMember */
479
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
480
        /** @var Group $memberlessGroup */
481
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
482
483
        $this->assertFalse($grouplessMember->Groups()->exists());
484
        $this->assertFalse($memberlessGroup->Members()->exists());
485
486
        $grouplessMember->addToGroupByCode('memberless');
487
488
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
489
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
490
491
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
492
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
493
494
        /** @var Group $group */
495
        $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
496
        $this->assertNotNull($group);
497
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
498
        $this->assertEquals($group->Title, 'New Group');
499
500
        $grouplessMember->removeFromGroupByCode('memberless');
501
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
502
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
503
504
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
505
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
506
    }
507
508
    public function testInGroup()
509
    {
510
        /** @var Member $staffmember */
511
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
512
        /** @var Member $managementmember */
513
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
514
        /** @var Member $accountingmember */
515
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
516
        /** @var Member $ceomember */
517
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
518
519
        /** @var Group $staffgroup */
520
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
521
        /** @var Group $managementgroup */
522
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
523
        /** @var Group $ceogroup */
524
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
525
526
        $this->assertTrue(
527
            $staffmember->inGroup($staffgroup),
528
            'Direct group membership is detected'
529
        );
530
        $this->assertTrue(
531
            $managementmember->inGroup($staffgroup),
532
            'Users of child group are members of a direct parent group (if not in strict mode)'
533
        );
534
        $this->assertTrue(
535
            $accountingmember->inGroup($staffgroup),
536
            'Users of child group are members of a direct parent group (if not in strict mode)'
537
        );
538
        $this->assertTrue(
539
            $ceomember->inGroup($staffgroup),
540
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
541
        );
542
        $this->assertTrue(
543
            $ceomember->inGroup($ceogroup, true),
544
            'Direct group membership is dected (if in strict mode)'
545
        );
546
        $this->assertFalse(
547
            $ceomember->inGroup($staffgroup, true),
548
            'Users of child group are not members of a direct parent group (if in strict mode)'
549
        );
550
        $this->assertFalse(
551
            $staffmember->inGroup($managementgroup),
552
            'Users of parent group are not members of a direct child group'
553
        );
554
        $this->assertFalse(
555
            $staffmember->inGroup($ceogroup),
556
            'Users of parent group are not members of an indirect grandchild group'
557
        );
558
        $this->assertFalse(
559
            $accountingmember->inGroup($managementgroup),
560
            'Users of group are not members of any siblings'
561
        );
562
        $this->assertFalse(
563
            $staffmember->inGroup('does-not-exist'),
564
            'Non-existant group returns false'
565
        );
566
    }
567
568
    /**
569
     * Tests that the user is able to view their own record, and in turn, they can
570
     * edit and delete their own record too.
571
     */
572
    public function testCanManipulateOwnRecord()
573
    {
574
        $member = $this->objFromFixture(Member::class, 'test');
575
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
576
577
        /* Not logged in, you can't view, delete or edit the record */
578
        $this->assertFalse($member->canView());
579
        $this->assertFalse($member->canDelete());
580
        $this->assertFalse($member->canEdit());
581
582
        /* Logged in users can edit their own record */
583
        $this->logInAs($member);
584
        $this->assertTrue($member->canView());
585
        $this->assertFalse($member->canDelete());
586
        $this->assertTrue($member->canEdit());
587
588
        /* Other uses cannot view, delete or edit others records */
589
        $this->logInAs($member2);
590
        $this->assertFalse($member->canView());
591
        $this->assertFalse($member->canDelete());
592
        $this->assertFalse($member->canEdit());
593
594
        $this->logOut();
595
    }
596
597
    public function testAuthorisedMembersCanManipulateOthersRecords()
598
    {
599
        $member = $this->objFromFixture(Member::class, 'test');
600
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
601
602
        /* Group members with SecurityAdmin permissions can manipulate other records */
603
        $this->logInAs($member);
604
        $this->assertTrue($member2->canView());
605
        $this->assertTrue($member2->canDelete());
606
        $this->assertTrue($member2->canEdit());
607
608
        $this->logOut();
609
    }
610
611
    public function testExtendedCan()
612
    {
613
        $member = $this->objFromFixture(Member::class, 'test');
614
615
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
616
        $this->assertFalse($member->canView());
617
        $this->assertFalse($member->canDelete());
618
        $this->assertFalse($member->canEdit());
619
620
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
621
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
622
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
623
624
        $this->assertTrue($member2->canView());
625
        $this->assertFalse($member2->canDelete());
626
        $this->assertFalse($member2->canEdit());
627
628
        /* Apply a extension that denies viewing of the Member */
629
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
630
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
631
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
632
633
        $this->assertFalse($member3->canView());
634
        $this->assertFalse($member3->canDelete());
635
        $this->assertFalse($member3->canEdit());
636
637
        /* Apply a extension that allows viewing and editing but denies deletion */
638
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
639
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
640
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
641
642
        $this->assertTrue($member4->canView());
643
        $this->assertFalse($member4->canDelete());
644
        $this->assertTrue($member4->canEdit());
645
646
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
647
    }
648
649
    /**
650
     * Tests for {@link Member::getName()} and {@link Member::setName()}
651
     */
652
    public function testName()
653
    {
654
        /** @var Member $member */
655
        $member = $this->objFromFixture(Member::class, 'test');
656
        $member->setName('Test Some User');
657
        $this->assertEquals('Test Some User', $member->getName());
658
        $member->setName('Test');
659
        $this->assertEquals('Test', $member->getName());
660
        $member->FirstName = 'Test';
661
        $member->Surname = '';
662
        $this->assertEquals('Test', $member->getName());
663
    }
664
665
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
666
    {
667
        $adminMember = $this->objFromFixture(Member::class, 'admin');
668
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
669
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
670
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
671
672
        // Careful: Don't read as english language.
673
        // More precisely this should read canBeEditedBy()
674
675
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
676
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
677
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
678
679
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
680
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
681
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
682
    }
683
684
    public function testOnChangeGroups()
685
    {
686
        /** @var Group $staffGroup */
687
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
688
        /** @var Member $staffMember */
689
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
690
        /** @var Member $adminMember */
691
        $adminMember = $this->objFromFixture(Member::class, 'admin');
692
693
        // Construct admin and non-admin gruops
694
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
695
        $newAdminGroup->write();
696
        Permission::grant($newAdminGroup->ID, 'ADMIN');
697
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
698
        $newOtherGroup->write();
699
700
        $this->assertTrue(
701
            $staffMember->onChangeGroups(array($staffGroup->ID)),
702
            'Adding existing non-admin group relation is allowed for non-admin members'
703
        );
704
        $this->assertTrue(
705
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
706
            'Adding new non-admin group relation is allowed for non-admin members'
707
        );
708
        $this->assertFalse(
709
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
710
            'Adding new admin group relation is not allowed for non-admin members'
711
        );
712
713
        $this->logInAs($adminMember);
714
        $this->assertTrue(
715
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
716
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
717
        );
718
        $this->logOut();
719
720
        $this->assertTrue(
721
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
722
            'Adding new admin group relation is allowed for admin members'
723
        );
724
    }
725
726
    /**
727
     * Ensure DirectGroups listbox disallows admin-promotion
728
     */
729
    public function testAllowedGroupsListbox()
730
    {
731
        /** @var Group $adminGroup */
732
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
733
        /** @var Member $staffMember */
734
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
735
        /** @var Member $adminMember */
736
        $adminMember = $this->objFromFixture(Member::class, 'admin');
737
738
        // Ensure you can see the DirectGroups box
739
        $this->logInWithPermission('EDIT_PERMISSIONS');
740
741
        // Non-admin member field contains non-admin groups
742
        /** @var ListboxField $staffListbox */
743
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
744
        $this->assertArrayNotHasKey($adminGroup->ID, $staffListbox->getSource());
745
746
        // admin member field contains admin group
747
        /** @var ListboxField $adminListbox */
748
        $adminListbox = $adminMember->getCMSFields()->dataFieldByName('DirectGroups');
749
        $this->assertArrayHasKey($adminGroup->ID, $adminListbox->getSource());
750
751
        // If logged in as admin, staff listbox has admin group
752
        $this->logInWithPermission('ADMIN');
753
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
754
        $this->assertArrayHasKey($adminGroup->ID, $staffListbox->getSource());
0 ignored issues
show
Bug introduced by
The method getSource() does not exist on SilverStripe\Forms\FormField. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

754
        $this->assertArrayHasKey($adminGroup->ID, $staffListbox->/** @scrutinizer ignore-call */ getSource());
Loading history...
755
    }
756
757
    /**
758
     * Test Member_GroupSet::add
759
     */
760
    public function testOnChangeGroupsByAdd()
761
    {
762
        /** @var Member $staffMember */
763
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
764
        /** @var Member $adminMember */
765
        $adminMember = $this->objFromFixture(Member::class, 'admin');
766
767
        // Setup new admin group
768
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
769
        $newAdminGroup->write();
770
        Permission::grant($newAdminGroup->ID, 'ADMIN');
771
772
        // Setup non-admin group
773
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
774
        $newOtherGroup->write();
775
776
        // Test staff can be added to other group
777
        $this->assertFalse($staffMember->inGroup($newOtherGroup));
778
        $staffMember->Groups()->add($newOtherGroup);
779
        $this->assertTrue(
780
            $staffMember->inGroup($newOtherGroup),
781
            'Adding new non-admin group relation is allowed for non-admin members'
782
        );
783
784
        // Test staff member can't be added to admin groups
785
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
786
        $staffMember->Groups()->add($newAdminGroup);
787
        $this->assertFalse(
788
            $staffMember->inGroup($newAdminGroup),
789
            'Adding new admin group relation is not allowed for non-admin members'
790
        );
791
792
        // Test staff member can be added to admin group by admins
793
        $this->logInAs($adminMember);
794
        $staffMember->Groups()->add($newAdminGroup);
795
        $this->assertTrue(
796
            $staffMember->inGroup($newAdminGroup),
797
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
798
        );
799
800
        // Test staff member can be added if they are already admin
801
        $this->logOut();
802
        $this->assertFalse($adminMember->inGroup($newAdminGroup));
803
        $adminMember->Groups()->add($newAdminGroup);
804
        $this->assertTrue(
805
            $adminMember->inGroup($newAdminGroup),
806
            'Adding new admin group relation is allowed for admin members'
807
        );
808
    }
809
810
    /**
811
     * Test Member_GroupSet::add
812
     */
813
    public function testOnChangeGroupsBySetIDList()
814
    {
815
        /** @var Member $staffMember */
816
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
817
818
        // Setup new admin group
819
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
820
        $newAdminGroup->write();
821
        Permission::grant($newAdminGroup->ID, 'ADMIN');
822
823
        // Test staff member can't be added to admin groups
824
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
825
        $staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
826
        $this->assertFalse(
827
            $staffMember->inGroup($newAdminGroup),
828
            'Adding new admin group relation is not allowed for non-admin members'
829
        );
830
    }
831
832
    /**
833
     * Test that extensions using updateCMSFields() are applied correctly
834
     */
835
    public function testUpdateCMSFields()
836
    {
837
        Member::add_extension(FieldsExtension::class);
838
839
        $member = Member::singleton();
840
        $fields = $member->getCMSFields();
841
842
        /**
843
 * @skipUpgrade
844
*/
845
        $this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
846
        $this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
847
        $this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
848
849
        Member::remove_extension(FieldsExtension::class);
850
    }
851
852
    /**
853
     * Test that all members are returned
854
     */
855
    public function testMap_in_groupsReturnsAll()
856
    {
857
        $members = Member::map_in_groups();
858
        $this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
859
    }
860
861
    /**
862
     * Test that only admin members are returned
863
     */
864
    public function testMap_in_groupsReturnsAdmins()
865
    {
866
        $adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
867
        $members = Member::map_in_groups($adminID)->toArray();
868
869
        $admin = $this->objFromFixture(Member::class, 'admin');
870
        $otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
871
872
        $this->assertTrue(
873
            in_array($admin->getTitle(), $members),
874
            $admin->getTitle() . ' should be in the returned list.'
875
        );
876
        $this->assertTrue(
877
            in_array($otherAdmin->getTitle(), $members),
878
            $otherAdmin->getTitle() . ' should be in the returned list.'
879
        );
880
        $this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
881
    }
882
883
    /**
884
     * Add the given array of member extensions as class names.
885
     * This is useful for re-adding extensions after being removed
886
     * in a test case to produce an unbiased test.
887
     *
888
     * @param  array $extensions
889
     * @return array The added extensions
890
     */
891
    protected function addExtensions($extensions)
892
    {
893
        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...
894
            foreach ($extensions as $extension) {
895
                Member::add_extension($extension);
896
            }
897
        }
898
        return $extensions;
899
    }
900
901
    /**
902
     * Remove given extensions from Member. This is useful for
903
     * removing extensions that could produce a biased
904
     * test result, as some extensions applied by project
905
     * code or modules can do this.
906
     *
907
     * @param  array $extensions
908
     * @return array The removed extensions
909
     */
910
    protected function removeExtensions($extensions)
911
    {
912
        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...
913
            foreach ($extensions as $extension) {
914
                Member::remove_extension($extension);
915
            }
916
        }
917
        return $extensions;
918
    }
919
920
    public function testGenerateAutologinTokenAndStoreHash()
921
    {
922
        $m = new Member();
923
        $m->write();
924
925
        $token = $m->generateAutologinTokenAndStoreHash();
926
927
        $this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as a hash.');
928
    }
929
930
    public function testValidateAutoLoginToken()
931
    {
932
        $enc = new PasswordEncryptor_Blowfish();
0 ignored issues
show
Unused Code introduced by
The assignment to $enc is dead and can be removed.
Loading history...
933
934
        $m1 = new Member();
935
        $m1->write();
936
        $m1Token = $m1->generateAutologinTokenAndStoreHash();
937
938
        $m2 = new Member();
939
        $m2->write();
940
        $m2->generateAutologinTokenAndStoreHash();
941
942
        $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
943
        $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
944
    }
945
946
    public function testRememberMeHashGeneration()
947
    {
948
        /** @var Member $m1 */
949
        $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
950
951
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
952
953
        $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
954
        $this->assertEquals($hashes->count(), 1);
955
        /** @var RememberLoginHash $firstHash */
956
        $firstHash = $hashes->first();
957
        $this->assertNotNull($firstHash->DeviceID);
958
        $this->assertNotNull($firstHash->Hash);
959
    }
960
961
    public function testRememberMeHashAutologin()
962
    {
963
        /** @var Member $m1 */
964
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
965
966
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
967
968
        /** @var RememberLoginHash $firstHash */
969
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
970
        $this->assertNotNull($firstHash);
971
972
        // re-generates the hash so we can get the token
973
        $firstHash->Hash = $firstHash->getNewHash($m1);
974
        $token = $firstHash->getToken();
975
        $firstHash->write();
976
977
        $response = $this->get(
978
            'Security/login',
979
            $this->session(),
980
            null,
981
            array(
982
                'alc_enc' => $m1->ID . ':' . $token,
983
                'alc_device' => $firstHash->DeviceID
984
            )
985
        );
986
        $message = Convert::raw2xml(
987
            _t(
988
                'SilverStripe\\Security\\Member.LOGGEDINAS',
989
                "You're logged in as {name}.",
990
                array('name' => $m1->FirstName)
991
            )
992
        );
993
        $this->assertContains($message, $response->getBody());
994
995
        $this->logOut();
996
997
        // A wrong token or a wrong device ID should not let us autologin
998
        $response = $this->get(
999
            'Security/login',
1000
            $this->session(),
1001
            null,
1002
            array(
1003
                'alc_enc' => $m1->ID . ':asdfasd' . str_rot13($token),
1004
                'alc_device' => $firstHash->DeviceID
1005
            )
1006
        );
1007
        $this->assertNotContains($message, $response->getBody());
1008
1009
        $response = $this->get(
1010
            'Security/login',
1011
            $this->session(),
1012
            null,
1013
            array(
1014
                'alc_enc' => $m1->ID . ':' . $token,
1015
                'alc_device' => str_rot13($firstHash->DeviceID)
1016
            )
1017
        );
1018
        $this->assertNotContains($message, $response->getBody());
1019
1020
        // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
1021
        // should remove all previous hashes for this device
1022
        $response = $this->post(
1023
            'Security/login/default/LoginForm',
1024
            array(
1025
                'Email' => $m1->Email,
1026
                'Password' => '1nitialPassword',
1027
                'action_doLogin' => 'action_doLogin'
1028
            ),
1029
            null,
1030
            $this->session(),
1031
            null,
1032
            array(
1033
                'alc_device' => $firstHash->DeviceID
1034
            )
1035
        );
1036
        $this->assertContains($message, $response->getBody());
1037
        $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
1038
    }
1039
1040
    public function testExpiredRememberMeHashAutologin()
1041
    {
1042
        /** @var Member $m1 */
1043
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1044
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1045
        /** @var RememberLoginHash $firstHash */
1046
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1047
        $this->assertNotNull($firstHash);
1048
1049
        // re-generates the hash so we can get the token
1050
        $firstHash->Hash = $firstHash->getNewHash($m1);
1051
        $token = $firstHash->getToken();
1052
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1053
        $firstHash->write();
1054
1055
        DBDatetime::set_mock_now('1999-12-31 23:59:59');
1056
1057
        $response = $this->get(
1058
            'Security/login',
1059
            $this->session(),
1060
            null,
1061
            array(
1062
                'alc_enc' => $m1->ID . ':' . $token,
1063
                'alc_device' => $firstHash->DeviceID
1064
            )
1065
        );
1066
        $message = Convert::raw2xml(
1067
            _t(
1068
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1069
                "You're logged in as {name}.",
1070
                array('name' => $m1->FirstName)
1071
            )
1072
        );
1073
        $this->assertContains($message, $response->getBody());
1074
1075
        $this->logOut();
1076
1077
        // re-generates the hash so we can get the token
1078
        $firstHash->Hash = $firstHash->getNewHash($m1);
1079
        $token = $firstHash->getToken();
1080
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1081
        $firstHash->write();
1082
1083
        DBDatetime::set_mock_now('2000-01-01 00:00:01');
1084
1085
        $response = $this->get(
1086
            'Security/login',
1087
            $this->session(),
1088
            null,
1089
            array(
1090
                'alc_enc' => $m1->ID . ':' . $token,
1091
                'alc_device' => $firstHash->DeviceID
1092
            )
1093
        );
1094
        $this->assertNotContains($message, $response->getBody());
1095
        $this->logOut();
1096
        DBDatetime::clear_mock_now();
1097
    }
1098
1099
    public function testRememberMeMultipleDevices()
1100
    {
1101
        /** @var Member $m1 */
1102
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1103
1104
        // First device
1105
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1106
        Cookie::set('alc_device', null);
1107
        // Second device
1108
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1109
1110
        // Hash of first device
1111
        /** @var RememberLoginHash $firstHash */
1112
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1113
        $this->assertNotNull($firstHash);
1114
1115
        // Hash of second device
1116
        /** @var RememberLoginHash $secondHash */
1117
        $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
1118
        $this->assertNotNull($secondHash);
1119
1120
        // DeviceIDs are different
1121
        $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1122
1123
        // re-generates the hashes so we can get the tokens
1124
        $firstHash->Hash = $firstHash->getNewHash($m1);
1125
        $firstToken = $firstHash->getToken();
1126
        $firstHash->write();
1127
1128
        $secondHash->Hash = $secondHash->getNewHash($m1);
1129
        $secondToken = $secondHash->getToken();
1130
        $secondHash->write();
1131
1132
        // Accessing the login page should show the user's name straight away
1133
        $response = $this->get(
1134
            'Security/login',
1135
            $this->session(),
1136
            null,
1137
            array(
1138
                'alc_enc' => $m1->ID . ':' . $firstToken,
1139
                'alc_device' => $firstHash->DeviceID
1140
            )
1141
        );
1142
        $message = Convert::raw2xml(
1143
            _t(
1144
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1145
                "You're logged in as {name}.",
1146
                array('name' => $m1->FirstName)
1147
            )
1148
        );
1149
        $this->assertContains($message, $response->getBody());
1150
1151
        // Test that removing session but not cookie keeps user
1152
        /** @var SessionAuthenticationHandler $sessionHandler */
1153
        $sessionHandler = Injector::inst()->get(SessionAuthenticationHandler::class);
1154
        $sessionHandler->logOut();
1155
        Security::setCurrentUser(null);
1156
1157
        // Accessing the login page from the second device
1158
        $response = $this->get(
1159
            'Security/login',
1160
            $this->session(),
1161
            null,
1162
            array(
1163
                'alc_enc' => $m1->ID . ':' . $secondToken,
1164
                'alc_device' => $secondHash->DeviceID
1165
            )
1166
        );
1167
        $this->assertContains($message, $response->getBody());
1168
1169
        // Logging out from the second device - only one device being logged out
1170
        RememberLoginHash::config()->update('logout_across_devices', false);
1171
        $this->get(
1172
            'Security/logout',
1173
            $this->session(),
1174
            null,
1175
            array(
1176
                'alc_enc' => $m1->ID . ':' . $secondToken,
1177
                'alc_device' => $secondHash->DeviceID
1178
            )
1179
        );
1180
        $this->assertEquals(
1181
            RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1182
            1
1183
        );
1184
1185
        // Logging out from any device when all login hashes should be removed
1186
        RememberLoginHash::config()->update('logout_across_devices', true);
1187
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1188
        $this->get('Security/logout', $this->session());
1189
        $this->assertEquals(
1190
            RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1191
            0
1192
        );
1193
    }
1194
1195
    public function testCanDelete()
1196
    {
1197
        $admin1 = $this->objFromFixture(Member::class, 'admin');
1198
        $admin2 = $this->objFromFixture(Member::class, 'other-admin');
1199
        $member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1200
        $member2 = $this->objFromFixture(Member::class, 'noformatmember');
1201
1202
        $this->assertTrue(
1203
            $admin1->canDelete($admin2),
1204
            'Admins can delete other admins'
1205
        );
1206
        $this->assertTrue(
1207
            $member1->canDelete($admin2),
1208
            'Admins can delete non-admins'
1209
        );
1210
        $this->assertFalse(
1211
            $admin1->canDelete($admin1),
1212
            'Admins can not delete themselves'
1213
        );
1214
        $this->assertFalse(
1215
            $member1->canDelete($member2),
1216
            'Non-admins can not delete other non-admins'
1217
        );
1218
        $this->assertFalse(
1219
            $member1->canDelete($member1),
1220
            'Non-admins can not delete themselves'
1221
        );
1222
    }
1223
1224
    public function testFailedLoginCount()
1225
    {
1226
        $maxFailedLoginsAllowed = 3;
1227
        //set up the config variables to enable login lockouts
1228
        Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1229
1230
        /** @var Member $member */
1231
        $member = $this->objFromFixture(Member::class, 'test');
1232
        $failedLoginCount = $member->FailedLoginCount;
1233
1234
        for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1235
            $member->registerFailedLogin();
1236
1237
            $this->assertEquals(
1238
                ++$failedLoginCount,
1239
                $member->FailedLoginCount,
1240
                'Failed to increment $member->FailedLoginCount'
1241
            );
1242
1243
            $this->assertTrue(
1244
                $member->canLogin(),
1245
                "Member has been locked out too early"
1246
            );
1247
        }
1248
    }
1249
1250
    public function testMemberValidator()
1251
    {
1252
        // clear custom requirements for this test
1253
        Member_Validator::config()->update('customRequired', null);
1254
        /** @var Member $memberA */
1255
        $memberA = $this->objFromFixture(Member::class, 'admin');
1256
        /** @var Member $memberB */
1257
        $memberB = $this->objFromFixture(Member::class, 'test');
1258
1259
        // create a blank form
1260
        $form = new MemberTest\ValidatorForm();
1261
1262
        $validator = new Member_Validator();
1263
        $validator->setForm($form);
1264
1265
        // Simulate creation of a new member via form, but use an existing member identifier
1266
        $fail = $validator->php(
1267
            array(
1268
            'FirstName' => 'Test',
1269
            'Email' => $memberA->Email
1270
            )
1271
        );
1272
1273
        $this->assertFalse(
1274
            $fail,
1275
            'Member_Validator must fail when trying to create new Member with existing Email.'
1276
        );
1277
1278
        // populate the form with values from another member
1279
        $form->loadDataFrom($memberB);
1280
1281
        // Assign the validator to an existing member
1282
        // (this is basically the same as passing the member ID with the form data)
1283
        $validator->setForMember($memberB);
1284
1285
        // Simulate update of a member via form and use an existing member Email
1286
        $fail = $validator->php(
1287
            array(
1288
            'FirstName' => 'Test',
1289
            'Email' => $memberA->Email
1290
            )
1291
        );
1292
1293
        // Simulate update to a new Email address
1294
        $pass1 = $validator->php(
1295
            array(
1296
            'FirstName' => 'Test',
1297
            'Email' => '[email protected]'
1298
            )
1299
        );
1300
1301
        // Pass in the same Email address that the member already has. Ensure that case is valid
1302
        $pass2 = $validator->php(
1303
            array(
1304
            'FirstName' => 'Test',
1305
            'Surname' => 'User',
1306
            'Email' => $memberB->Email
1307
            )
1308
        );
1309
1310
        $this->assertFalse(
1311
            $fail,
1312
            'Member_Validator must fail when trying to update existing member with existing Email.'
1313
        );
1314
1315
        $this->assertTrue(
1316
            $pass1,
1317
            'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1318
        );
1319
1320
        $this->assertTrue(
1321
            $pass2,
1322
            'Member_Validator must pass when Member updates his own Email to the already existing value.'
1323
        );
1324
    }
1325
1326
    public function testMemberValidatorWithExtensions()
1327
    {
1328
        // clear custom requirements for this test
1329
        Member_Validator::config()->update('customRequired', null);
1330
1331
        // create a blank form
1332
        $form = new MemberTest\ValidatorForm();
1333
1334
        // Test extensions
1335
        Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1336
        $validator = new Member_Validator();
1337
        $validator->setForm($form);
1338
1339
        // This test should fail, since the extension enforces FirstName == Surname
1340
        $fail = $validator->php(
1341
            array(
1342
            'FirstName' => 'Test',
1343
            'Surname' => 'User',
1344
            'Email' => '[email protected]'
1345
            )
1346
        );
1347
1348
        $pass = $validator->php(
1349
            array(
1350
            'FirstName' => 'Test',
1351
            'Surname' => 'Test',
1352
            'Email' => '[email protected]'
1353
            )
1354
        );
1355
1356
        $this->assertFalse(
1357
            $fail,
1358
            'Member_Validator must fail because of added extension.'
1359
        );
1360
1361
        $this->assertTrue(
1362
            $pass,
1363
            'Member_Validator must succeed, since it meets all requirements.'
1364
        );
1365
1366
        // Add another extension that always fails. This ensures that all extensions are considered in the validation
1367
        Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1368
        $validator = new Member_Validator();
1369
        $validator->setForm($form);
1370
1371
        // Even though the data is valid, This test should still fail, since one extension always returns false
1372
        $fail = $validator->php(
1373
            array(
1374
            'FirstName' => 'Test',
1375
            'Surname' => 'Test',
1376
            'Email' => '[email protected]'
1377
            )
1378
        );
1379
1380
        $this->assertFalse(
1381
            $fail,
1382
            'Member_Validator must fail because of added extensions.'
1383
        );
1384
1385
        // Remove added extensions
1386
        Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1387
        Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1388
    }
1389
1390
    public function testCustomMemberValidator()
1391
    {
1392
        // clear custom requirements for this test
1393
        Member_Validator::config()->update('customRequired', null);
1394
1395
        $member = $this->objFromFixture(Member::class, 'admin');
1396
1397
        $form = new MemberTest\ValidatorForm();
1398
        $form->loadDataFrom($member);
1399
1400
        $validator = new Member_Validator();
1401
        $validator->setForm($form);
1402
1403
        $pass = $validator->php(
1404
            array(
1405
            'FirstName' => 'Borris',
1406
            'Email' => '[email protected]'
1407
            )
1408
        );
1409
1410
        $fail = $validator->php(
1411
            array(
1412
            'Email' => '[email protected]',
1413
            'Surname' => ''
1414
            )
1415
        );
1416
1417
        $this->assertTrue($pass, 'Validator requires a FirstName and Email');
1418
        $this->assertFalse($fail, 'Missing FirstName');
1419
1420
        $ext = new MemberTest\ValidatorExtension();
1421
        $ext->updateValidator($validator);
1422
1423
        $pass = $validator->php(
1424
            array(
1425
            'FirstName' => 'Borris',
1426
            'Email' => '[email protected]'
1427
            )
1428
        );
1429
1430
        $fail = $validator->php(
1431
            array(
1432
            'Email' => '[email protected]'
1433
            )
1434
        );
1435
1436
        $this->assertFalse($pass, 'Missing surname');
1437
        $this->assertFalse($fail, 'Missing surname value');
1438
1439
        $fail = $validator->php(
1440
            array(
1441
            'Email' => '[email protected]',
1442
            'Surname' => 'Silverman'
1443
            )
1444
        );
1445
1446
        $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1447
    }
1448
1449
    public function testCurrentUser()
1450
    {
1451
        $this->assertNull(Security::getCurrentUser());
1452
1453
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1454
        $this->logInAs($adminMember);
1455
1456
        $userFromSession = Security::getCurrentUser();
1457
        $this->assertEquals($adminMember->ID, $userFromSession->ID);
1458
    }
1459
1460
    /**
1461
     * @covers \SilverStripe\Security\Member::actAs()
1462
     */
1463
    public function testActAsUserPermissions()
1464
    {
1465
        $this->assertNull(Security::getCurrentUser());
1466
1467
        /** @var Member $adminMember */
1468
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1469
1470
        // Check acting as admin when not logged in
1471
        $checkAdmin = Member::actAs($adminMember, function () {
1472
            return Permission::check('ADMIN');
1473
        });
1474
        $this->assertTrue($checkAdmin);
1475
1476
        // Check nesting
1477
        $checkAdmin = Member::actAs($adminMember, function () {
1478
            return Member::actAs(null, function () {
1479
                return Permission::check('ADMIN');
1480
            });
1481
        });
1482
        $this->assertFalse($checkAdmin);
1483
1484
        // Check logging in as non-admin user
1485
        $this->logInWithPermission('TEST_PERMISSION');
1486
1487
        $hasPerm = Member::actAs(null, function () {
1488
            return Permission::check('TEST_PERMISSION');
1489
        });
1490
        $this->assertFalse($hasPerm);
1491
1492
        // Check permissions can be promoted
1493
        $checkAdmin = Member::actAs($adminMember, function () {
1494
            return Permission::check('ADMIN');
1495
        });
1496
        $this->assertTrue($checkAdmin);
1497
    }
1498
1499
    /**
1500
     * @covers \SilverStripe\Security\Member::actAs()
1501
     */
1502
    public function testActAsUser()
1503
    {
1504
        $this->assertNull(Security::getCurrentUser());
1505
1506
        /** @var Member $adminMember */
1507
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1508
        $member = Member::actAs($adminMember, function () {
1509
            return Security::getCurrentUser();
1510
        });
1511
        $this->assertEquals($adminMember->ID, $member->ID);
1512
1513
        // Check nesting
1514
        $member = Member::actAs($adminMember, function () {
1515
            return Member::actAs(null, function () {
1516
                return Security::getCurrentUser();
1517
            });
1518
        });
1519
        $this->assertEmpty($member);
1520
    }
1521
1522
    public function testChangePasswordWithExtensionsThatModifyValidationResult()
1523
    {
1524
        // Default behaviour
1525
        /** @var Member $member */
1526
        $member = $this->objFromFixture(Member::class, 'admin');
1527
        $result = $member->changePassword('my-secret-new-password');
1528
        $this->assertInstanceOf(ValidationResult::class, $result);
1529
        $this->assertTrue($result->isValid());
1530
1531
        // With an extension added
1532
        Member::add_extension(MemberTest\ExtendedChangePasswordExtension::class);
1533
        $member = $this->objFromFixture(Member::class, 'admin');
1534
        $result = $member->changePassword('my-second-secret-password');
0 ignored issues
show
Bug introduced by
The method changePassword() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1534
        /** @scrutinizer ignore-call */ 
1535
        $result = $member->changePassword('my-second-secret-password');
Loading history...
1535
        $this->assertInstanceOf(ValidationResult::class, $result);
1536
        $this->assertFalse($result->isValid());
1537
    }
1538
1539
    public function testNewMembersReceiveTheDefaultLocale()
1540
    {
1541
        // Set a different current locale to the default
1542
        i18n::set_locale('de_DE');
1543
1544
        $newMember = Member::create();
1545
        $newMember->update([
1546
            'FirstName' => 'Leslie',
1547
            'Surname' => 'Longly',
1548
            'Email' => '[email protected]',
1549
        ]);
1550
        $newMember->write();
1551
1552
        $this->assertSame('en_US', $newMember->Locale, 'New members receive the default locale');
1553
    }
1554
}
1555