Passed
Push — 4.0.4 ( fe4f6f )
by Robbie
09:31
created

MemberTest::testUpdateCMSFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

375
        $this->assertFalse($member->/** @scrutinizer ignore-call */ isPasswordExpired());
Loading history...
376
377
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
378
        $this->assertTrue($member->isPasswordExpired());
379
380
        // Check the boundary conditions
381
        // If PasswordExpiry == today, then it's expired
382
        $member->PasswordExpiry = date('Y-m-d');
383
        $this->assertTrue($member->isPasswordExpired());
384
385
        // If PasswordExpiry == tomorrow, then it's not
386
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
387
        $this->assertFalse($member->isPasswordExpired());
388
    }
389
    public function testInGroups()
390
    {
391
        /** @var Member $staffmember */
392
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
393
        /** @var Member $ceomember */
394
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
395
396
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
397
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
398
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
399
400
        $this->assertTrue(
401
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
402
            'inGroups() succeeds if a membership is detected on one of many passed groups'
403
        );
404
        $this->assertFalse(
405
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
406
            'inGroups() fails if a membership is detected on none of the passed groups'
407
        );
408
        $this->assertFalse(
409
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
410
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
411
        );
412
    }
413
414
    public function testAddToGroupByCode()
415
    {
416
        /** @var Member $grouplessMember */
417
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
418
        /** @var Group $memberlessGroup */
419
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
420
421
        $this->assertFalse($grouplessMember->Groups()->exists());
422
        $this->assertFalse($memberlessGroup->Members()->exists());
423
424
        $grouplessMember->addToGroupByCode('memberless');
425
426
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
427
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
428
429
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
430
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
431
432
        /** @var Group $group */
433
        $group = DataObject::get_one(
434
            Group::class,
435
            array(
436
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
437
            )
438
        );
439
        $this->assertNotNull($group);
440
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
441
        $this->assertEquals($group->Title, 'New Group');
442
    }
443
444
    public function testRemoveFromGroupByCode()
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(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
464
        $this->assertNotNull($group);
465
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
466
        $this->assertEquals($group->Title, 'New Group');
467
468
        $grouplessMember->removeFromGroupByCode('memberless');
469
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
470
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
471
472
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
473
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
474
    }
475
476
    public function testInGroup()
477
    {
478
        /** @var Member $staffmember */
479
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
480
        /** @var Member $managementmember */
481
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
482
        /** @var Member $accountingmember */
483
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
484
        /** @var Member $ceomember */
485
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
486
487
        /** @var Group $staffgroup */
488
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
489
        /** @var Group $managementgroup */
490
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
491
        /** @var Group $ceogroup */
492
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
493
494
        $this->assertTrue(
495
            $staffmember->inGroup($staffgroup),
496
            'Direct group membership is detected'
497
        );
498
        $this->assertTrue(
499
            $managementmember->inGroup($staffgroup),
500
            'Users of child group are members of a direct parent group (if not in strict mode)'
501
        );
502
        $this->assertTrue(
503
            $accountingmember->inGroup($staffgroup),
504
            'Users of child group are members of a direct parent group (if not in strict mode)'
505
        );
506
        $this->assertTrue(
507
            $ceomember->inGroup($staffgroup),
508
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
509
        );
510
        $this->assertTrue(
511
            $ceomember->inGroup($ceogroup, true),
512
            'Direct group membership is dected (if in strict mode)'
513
        );
514
        $this->assertFalse(
515
            $ceomember->inGroup($staffgroup, true),
516
            'Users of child group are not members of a direct parent group (if in strict mode)'
517
        );
518
        $this->assertFalse(
519
            $staffmember->inGroup($managementgroup),
520
            'Users of parent group are not members of a direct child group'
521
        );
522
        $this->assertFalse(
523
            $staffmember->inGroup($ceogroup),
524
            'Users of parent group are not members of an indirect grandchild group'
525
        );
526
        $this->assertFalse(
527
            $accountingmember->inGroup($managementgroup),
528
            'Users of group are not members of any siblings'
529
        );
530
        $this->assertFalse(
531
            $staffmember->inGroup('does-not-exist'),
532
            'Non-existant group returns false'
533
        );
534
    }
535
536
    /**
537
     * Tests that the user is able to view their own record, and in turn, they can
538
     * edit and delete their own record too.
539
     */
540
    public function testCanManipulateOwnRecord()
541
    {
542
        $member = $this->objFromFixture(Member::class, 'test');
543
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
544
545
        /* Not logged in, you can't view, delete or edit the record */
546
        $this->assertFalse($member->canView());
547
        $this->assertFalse($member->canDelete());
548
        $this->assertFalse($member->canEdit());
549
550
        /* Logged in users can edit their own record */
551
        $this->logInAs($member);
552
        $this->assertTrue($member->canView());
553
        $this->assertFalse($member->canDelete());
554
        $this->assertTrue($member->canEdit());
555
556
        /* Other uses cannot view, delete or edit others records */
557
        $this->logInAs($member2);
558
        $this->assertFalse($member->canView());
559
        $this->assertFalse($member->canDelete());
560
        $this->assertFalse($member->canEdit());
561
562
        $this->logOut();
563
    }
564
565
    public function testAuthorisedMembersCanManipulateOthersRecords()
566
    {
567
        $member = $this->objFromFixture(Member::class, 'test');
568
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
569
570
        /* Group members with SecurityAdmin permissions can manipulate other records */
571
        $this->logInAs($member);
572
        $this->assertTrue($member2->canView());
573
        $this->assertTrue($member2->canDelete());
574
        $this->assertTrue($member2->canEdit());
575
576
        $this->logOut();
577
    }
578
579
    public function testExtendedCan()
580
    {
581
        $member = $this->objFromFixture(Member::class, 'test');
582
583
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
584
        $this->assertFalse($member->canView());
585
        $this->assertFalse($member->canDelete());
586
        $this->assertFalse($member->canEdit());
587
588
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
589
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
590
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
591
592
        $this->assertTrue($member2->canView());
593
        $this->assertFalse($member2->canDelete());
594
        $this->assertFalse($member2->canEdit());
595
596
        /* Apply a extension that denies viewing of the Member */
597
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
598
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
599
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
600
601
        $this->assertFalse($member3->canView());
602
        $this->assertFalse($member3->canDelete());
603
        $this->assertFalse($member3->canEdit());
604
605
        /* Apply a extension that allows viewing and editing but denies deletion */
606
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
607
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
608
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
609
610
        $this->assertTrue($member4->canView());
611
        $this->assertFalse($member4->canDelete());
612
        $this->assertTrue($member4->canEdit());
613
614
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
615
    }
616
617
    /**
618
     * Tests for {@link Member::getName()} and {@link Member::setName()}
619
     */
620
    public function testName()
621
    {
622
        /** @var Member $member */
623
        $member = $this->objFromFixture(Member::class, 'test');
624
        $member->setName('Test Some User');
625
        $this->assertEquals('Test Some User', $member->getName());
626
        $member->setName('Test');
627
        $this->assertEquals('Test', $member->getName());
628
        $member->FirstName = 'Test';
629
        $member->Surname = '';
630
        $this->assertEquals('Test', $member->getName());
631
    }
632
633
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
634
    {
635
        $adminMember = $this->objFromFixture(Member::class, 'admin');
636
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
637
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
638
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
639
640
        // Careful: Don't read as english language.
641
        // More precisely this should read canBeEditedBy()
642
643
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
644
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
645
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
646
647
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
648
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
649
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
650
    }
651
652
    public function testOnChangeGroups()
653
    {
654
        /** @var Group $staffGroup */
655
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
656
        /** @var Member $staffMember */
657
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
658
        /** @var Member $adminMember */
659
        $adminMember = $this->objFromFixture(Member::class, 'admin');
660
661
        // Construct admin and non-admin gruops
662
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
663
        $newAdminGroup->write();
664
        Permission::grant($newAdminGroup->ID, 'ADMIN');
665
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
666
        $newOtherGroup->write();
667
668
        $this->assertTrue(
669
            $staffMember->onChangeGroups(array($staffGroup->ID)),
670
            'Adding existing non-admin group relation is allowed for non-admin members'
671
        );
672
        $this->assertTrue(
673
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
674
            'Adding new non-admin group relation is allowed for non-admin members'
675
        );
676
        $this->assertFalse(
677
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
678
            'Adding new admin group relation is not allowed for non-admin members'
679
        );
680
681
        $this->logInAs($adminMember);
682
        $this->assertTrue(
683
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
684
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
685
        );
686
        $this->logOut();
687
688
        $this->assertTrue(
689
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
690
            'Adding new admin group relation is allowed for admin members'
691
        );
692
    }
693
694
    /**
695
     * Ensure DirectGroups listbox disallows admin-promotion
696
     */
697
    public function testAllowedGroupsListbox()
698
    {
699
        /** @var Group $adminGroup */
700
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
701
        /** @var Member $staffMember */
702
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
703
        /** @var Member $adminMember */
704
        $adminMember = $this->objFromFixture(Member::class, 'admin');
705
706
        // Ensure you can see the DirectGroups box
707
        $this->logInWithPermission('EDIT_PERMISSIONS');
708
709
        // Non-admin member field contains non-admin groups
710
        /** @var ListboxField $staffListbox */
711
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
712
        $this->assertArrayNotHasKey($adminGroup->ID, $staffListbox->getSource());
713
714
        // admin member field contains admin group
715
        /** @var ListboxField $adminListbox */
716
        $adminListbox = $adminMember->getCMSFields()->dataFieldByName('DirectGroups');
717
        $this->assertArrayHasKey($adminGroup->ID, $adminListbox->getSource());
718
719
        // If logged in as admin, staff listbox has admin group
720
        $this->logInWithPermission('ADMIN');
721
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
722
        $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

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

1509
        /** @scrutinizer ignore-call */ 
1510
        $result = $member->changePassword('my-second-secret-password');
Loading history...
1510
        $this->assertInstanceOf(ValidationResult::class, $result);
1511
        $this->assertFalse($result->isValid());
1512
    }
1513
}
1514