Passed
Push — 4.1.1 ( 01ed8a )
by Robbie
09:45
created

MemberTest::setUp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

426
            $staffmember->/** @scrutinizer ignore-call */ 
427
                          inGroups(array($staffgroup, $managementgroup)),
Loading history...
427
            'inGroups() succeeds if a membership is detected on one of many passed groups'
428
        );
429
430
        $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

430
        $staffmember->/** @scrutinizer ignore-call */ 
431
                      Groups()->remove($managementgroup);
Loading history...
431
        $this->assertFalse(
432
            $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

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

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

1532
        /** @scrutinizer ignore-call */ 
1533
        $result = $member->changePassword('my-second-secret-password');
Loading history...
1533
        $this->assertInstanceOf(ValidationResult::class, $result);
1534
        $this->assertFalse($result->isValid());
1535
    }
1536
}
1537