Passed
Push — 4.0 ( 7733e9...1c72d6 )
by Daniel
07:30
created

testWriteDoesntMergeExistingMemberOnIdentifierChange()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

374
        $this->assertFalse($member->/** @scrutinizer ignore-call */ isPasswordExpired());
Loading history...
375
376
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
377
        $this->assertTrue($member->isPasswordExpired());
378
379
        // Check the boundary conditions
380
        // If PasswordExpiry == today, then it's expired
381
        $member->PasswordExpiry = date('Y-m-d');
382
        $this->assertTrue($member->isPasswordExpired());
383
384
        // If PasswordExpiry == tomorrow, then it's not
385
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
386
        $this->assertFalse($member->isPasswordExpired());
387
    }
388
    public function testInGroups()
389
    {
390
        /** @var Member $staffmember */
391
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
392
        /** @var Member $ceomember */
393
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
394
395
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
396
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
397
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
398
399
        $this->assertTrue(
400
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
401
            'inGroups() succeeds if a membership is detected on one of many passed groups'
402
        );
403
        $this->assertFalse(
404
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
405
            'inGroups() fails if a membership is detected on none of the passed groups'
406
        );
407
        $this->assertFalse(
408
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
409
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
410
        );
411
    }
412
413
    public function testAddToGroupByCode()
414
    {
415
        /** @var Member $grouplessMember */
416
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
417
        /** @var Group $memberlessGroup */
418
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
419
420
        $this->assertFalse($grouplessMember->Groups()->exists());
421
        $this->assertFalse($memberlessGroup->Members()->exists());
422
423
        $grouplessMember->addToGroupByCode('memberless');
424
425
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
426
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
427
428
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
429
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
430
431
        /** @var Group $group */
432
        $group = DataObject::get_one(
433
            Group::class,
434
            array(
435
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
436
            )
437
        );
438
        $this->assertNotNull($group);
439
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
440
        $this->assertEquals($group->Title, 'New Group');
441
    }
442
443
    public function testRemoveFromGroupByCode()
444
    {
445
        /** @var Member $grouplessMember */
446
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
447
        /** @var Group $memberlessGroup */
448
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
449
450
        $this->assertFalse($grouplessMember->Groups()->exists());
451
        $this->assertFalse($memberlessGroup->Members()->exists());
452
453
        $grouplessMember->addToGroupByCode('memberless');
454
455
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
456
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
457
458
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
459
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
460
461
        /** @var Group $group */
462
        $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
463
        $this->assertNotNull($group);
464
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
465
        $this->assertEquals($group->Title, 'New Group');
466
467
        $grouplessMember->removeFromGroupByCode('memberless');
468
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
469
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
470
471
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
472
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
473
    }
474
475
    public function testInGroup()
476
    {
477
        /** @var Member $staffmember */
478
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
479
        /** @var Member $managementmember */
480
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
481
        /** @var Member $accountingmember */
482
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
483
        /** @var Member $ceomember */
484
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
485
486
        /** @var Group $staffgroup */
487
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
488
        /** @var Group $managementgroup */
489
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
490
        /** @var Group $ceogroup */
491
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
492
493
        $this->assertTrue(
494
            $staffmember->inGroup($staffgroup),
495
            'Direct group membership is detected'
496
        );
497
        $this->assertTrue(
498
            $managementmember->inGroup($staffgroup),
499
            'Users of child group are members of a direct parent group (if not in strict mode)'
500
        );
501
        $this->assertTrue(
502
            $accountingmember->inGroup($staffgroup),
503
            'Users of child group are members of a direct parent group (if not in strict mode)'
504
        );
505
        $this->assertTrue(
506
            $ceomember->inGroup($staffgroup),
507
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
508
        );
509
        $this->assertTrue(
510
            $ceomember->inGroup($ceogroup, true),
511
            'Direct group membership is dected (if in strict mode)'
512
        );
513
        $this->assertFalse(
514
            $ceomember->inGroup($staffgroup, true),
515
            'Users of child group are not members of a direct parent group (if in strict mode)'
516
        );
517
        $this->assertFalse(
518
            $staffmember->inGroup($managementgroup),
519
            'Users of parent group are not members of a direct child group'
520
        );
521
        $this->assertFalse(
522
            $staffmember->inGroup($ceogroup),
523
            'Users of parent group are not members of an indirect grandchild group'
524
        );
525
        $this->assertFalse(
526
            $accountingmember->inGroup($managementgroup),
527
            'Users of group are not members of any siblings'
528
        );
529
        $this->assertFalse(
530
            $staffmember->inGroup('does-not-exist'),
531
            'Non-existant group returns false'
532
        );
533
    }
534
535
    /**
536
     * Tests that the user is able to view their own record, and in turn, they can
537
     * edit and delete their own record too.
538
     */
539
    public function testCanManipulateOwnRecord()
540
    {
541
        $member = $this->objFromFixture(Member::class, 'test');
542
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
543
544
        /* Not logged in, you can't view, delete or edit the record */
545
        $this->assertFalse($member->canView());
546
        $this->assertFalse($member->canDelete());
547
        $this->assertFalse($member->canEdit());
548
549
        /* Logged in users can edit their own record */
550
        $this->logInAs($member);
551
        $this->assertTrue($member->canView());
552
        $this->assertFalse($member->canDelete());
553
        $this->assertTrue($member->canEdit());
554
555
        /* Other uses cannot view, delete or edit others records */
556
        $this->logInAs($member2);
557
        $this->assertFalse($member->canView());
558
        $this->assertFalse($member->canDelete());
559
        $this->assertFalse($member->canEdit());
560
561
        $this->logOut();
562
    }
563
564
    public function testAuthorisedMembersCanManipulateOthersRecords()
565
    {
566
        $member = $this->objFromFixture(Member::class, 'test');
567
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
568
569
        /* Group members with SecurityAdmin permissions can manipulate other records */
570
        $this->logInAs($member);
571
        $this->assertTrue($member2->canView());
572
        $this->assertTrue($member2->canDelete());
573
        $this->assertTrue($member2->canEdit());
574
575
        $this->logOut();
576
    }
577
578
    public function testExtendedCan()
579
    {
580
        $member = $this->objFromFixture(Member::class, 'test');
581
582
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
583
        $this->assertFalse($member->canView());
584
        $this->assertFalse($member->canDelete());
585
        $this->assertFalse($member->canEdit());
586
587
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
588
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
589
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
590
591
        $this->assertTrue($member2->canView());
592
        $this->assertFalse($member2->canDelete());
593
        $this->assertFalse($member2->canEdit());
594
595
        /* Apply a extension that denies viewing of the Member */
596
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
597
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
598
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
599
600
        $this->assertFalse($member3->canView());
601
        $this->assertFalse($member3->canDelete());
602
        $this->assertFalse($member3->canEdit());
603
604
        /* Apply a extension that allows viewing and editing but denies deletion */
605
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
606
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
607
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
608
609
        $this->assertTrue($member4->canView());
610
        $this->assertFalse($member4->canDelete());
611
        $this->assertTrue($member4->canEdit());
612
613
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
614
    }
615
616
    /**
617
     * Tests for {@link Member::getName()} and {@link Member::setName()}
618
     */
619
    public function testName()
620
    {
621
        /** @var Member $member */
622
        $member = $this->objFromFixture(Member::class, 'test');
623
        $member->setName('Test Some User');
624
        $this->assertEquals('Test Some User', $member->getName());
625
        $member->setName('Test');
626
        $this->assertEquals('Test', $member->getName());
627
        $member->FirstName = 'Test';
628
        $member->Surname = '';
629
        $this->assertEquals('Test', $member->getName());
630
    }
631
632
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
633
    {
634
        $adminMember = $this->objFromFixture(Member::class, 'admin');
635
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
636
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
637
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
638
639
        // Careful: Don't read as english language.
640
        // More precisely this should read canBeEditedBy()
641
642
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
643
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
644
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
645
646
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
647
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
648
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
649
    }
650
651
    public function testOnChangeGroups()
652
    {
653
        /** @var Group $staffGroup */
654
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
655
        /** @var Member $staffMember */
656
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
657
        /** @var Member $adminMember */
658
        $adminMember = $this->objFromFixture(Member::class, 'admin');
659
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
660
        $newAdminGroup->write();
661
        Permission::grant($newAdminGroup->ID, 'ADMIN');
662
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
663
        $newOtherGroup->write();
664
665
        $this->assertTrue(
666
            $staffMember->onChangeGroups(array($staffGroup->ID)),
667
            'Adding existing non-admin group relation is allowed for non-admin members'
668
        );
669
        $this->assertTrue(
670
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
671
            'Adding new non-admin group relation is allowed for non-admin members'
672
        );
673
        $this->assertFalse(
674
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
675
            'Adding new admin group relation is not allowed for non-admin members'
676
        );
677
678
        $this->logInAs($adminMember);
679
        $this->assertTrue(
680
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
681
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
682
        );
683
        $this->logOut();
684
685
        $this->assertTrue(
686
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
687
            'Adding new admin group relation is allowed for admin members'
688
        );
689
    }
690
691
    /**
692
     * Test Member_GroupSet::add
693
     */
694
    public function testOnChangeGroupsByAdd()
695
    {
696
        /** @var Member $staffMember */
697
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
698
        /** @var Member $adminMember */
699
        $adminMember = $this->objFromFixture(Member::class, 'admin');
700
701
        // Setup new admin group
702
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
703
        $newAdminGroup->write();
704
        Permission::grant($newAdminGroup->ID, 'ADMIN');
705
706
        // Setup non-admin group
707
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
708
        $newOtherGroup->write();
709
710
        // Test staff can be added to other group
711
        $this->assertFalse($staffMember->inGroup($newOtherGroup));
712
        $staffMember->Groups()->add($newOtherGroup);
713
        $this->assertTrue(
714
            $staffMember->inGroup($newOtherGroup),
715
            'Adding new non-admin group relation is allowed for non-admin members'
716
        );
717
718
        // Test staff member can't be added to admin groups
719
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
720
        $staffMember->Groups()->add($newAdminGroup);
721
        $this->assertFalse(
722
            $staffMember->inGroup($newAdminGroup),
723
            'Adding new admin group relation is not allowed for non-admin members'
724
        );
725
726
        // Test staff member can be added to admin group by admins
727
        $this->logInAs($adminMember);
728
        $staffMember->Groups()->add($newAdminGroup);
729
        $this->assertTrue(
730
            $staffMember->inGroup($newAdminGroup),
731
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
732
        );
733
734
        // Test staff member can be added if they are already admin
735
        $this->logOut();
736
        $this->assertFalse($adminMember->inGroup($newAdminGroup));
737
        $adminMember->Groups()->add($newAdminGroup);
738
        $this->assertTrue(
739
            $adminMember->inGroup($newAdminGroup),
740
            'Adding new admin group relation is allowed for admin members'
741
        );
742
    }
743
744
    /**
745
     * Test Member_GroupSet::add
746
     */
747
    public function testOnChangeGroupsBySetIDList()
748
    {
749
        /** @var Member $staffMember */
750
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
751
752
        // Setup new admin group
753
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
754
        $newAdminGroup->write();
755
        Permission::grant($newAdminGroup->ID, 'ADMIN');
756
757
        // Test staff member can't be added to admin groups
758
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
759
        $staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
760
        $this->assertFalse(
761
            $staffMember->inGroup($newAdminGroup),
762
            'Adding new admin group relation is not allowed for non-admin members'
763
        );
764
    }
765
766
    /**
767
     * Test that extensions using updateCMSFields() are applied correctly
768
     */
769
    public function testUpdateCMSFields()
770
    {
771
        Member::add_extension(FieldsExtension::class);
772
773
        $member = Member::singleton();
774
        $fields = $member->getCMSFields();
775
776
        /**
777
 * @skipUpgrade
778
*/
779
        $this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
780
        $this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
781
        $this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
782
783
        Member::remove_extension(FieldsExtension::class);
784
    }
785
786
    /**
787
     * Test that all members are returned
788
     */
789
    public function testMap_in_groupsReturnsAll()
790
    {
791
        $members = Member::map_in_groups();
792
        $this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
793
    }
794
795
    /**
796
     * Test that only admin members are returned
797
     */
798
    public function testMap_in_groupsReturnsAdmins()
799
    {
800
        $adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
801
        $members = Member::map_in_groups($adminID)->toArray();
802
803
        $admin = $this->objFromFixture(Member::class, 'admin');
804
        $otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
805
806
        $this->assertTrue(
807
            in_array($admin->getTitle(), $members),
808
            $admin->getTitle().' should be in the returned list.'
809
        );
810
        $this->assertTrue(
811
            in_array($otherAdmin->getTitle(), $members),
812
            $otherAdmin->getTitle().' should be in the returned list.'
813
        );
814
        $this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
815
    }
816
817
    /**
818
     * Add the given array of member extensions as class names.
819
     * This is useful for re-adding extensions after being removed
820
     * in a test case to produce an unbiased test.
821
     *
822
     * @param  array $extensions
823
     * @return array The added extensions
824
     */
825
    protected function addExtensions($extensions)
826
    {
827
        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...
828
            foreach ($extensions as $extension) {
829
                Member::add_extension($extension);
830
            }
831
        }
832
        return $extensions;
833
    }
834
835
    /**
836
     * Remove given extensions from Member. This is useful for
837
     * removing extensions that could produce a biased
838
     * test result, as some extensions applied by project
839
     * code or modules can do this.
840
     *
841
     * @param  array $extensions
842
     * @return array The removed extensions
843
     */
844
    protected function removeExtensions($extensions)
845
    {
846
        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...
847
            foreach ($extensions as $extension) {
848
                Member::remove_extension($extension);
849
            }
850
        }
851
        return $extensions;
852
    }
853
854
    public function testGenerateAutologinTokenAndStoreHash()
855
    {
856
        $enc = new PasswordEncryptor_Blowfish();
857
858
        $m = new Member();
859
        $m->PasswordEncryption = 'blowfish';
860
        $m->Salt = $enc->salt('123');
861
862
        $token = $m->generateAutologinTokenAndStoreHash();
863
864
        $this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
865
    }
866
867
    public function testValidateAutoLoginToken()
868
    {
869
        $enc = new PasswordEncryptor_Blowfish();
870
871
        $m1 = new Member();
872
        $m1->PasswordEncryption = 'blowfish';
873
        $m1->Salt = $enc->salt('123');
874
        $m1Token = $m1->generateAutologinTokenAndStoreHash();
875
876
        $m2 = new Member();
877
        $m2->PasswordEncryption = 'blowfish';
878
        $m2->Salt = $enc->salt('456');
879
        $m2->generateAutologinTokenAndStoreHash();
880
881
        $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
882
        $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
883
    }
884
885
    public function testRememberMeHashGeneration()
886
    {
887
        /** @var Member $m1 */
888
        $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
889
890
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
891
892
        $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
893
        $this->assertEquals($hashes->count(), 1);
894
        /** @var RememberLoginHash $firstHash */
895
        $firstHash = $hashes->first();
896
        $this->assertNotNull($firstHash->DeviceID);
897
        $this->assertNotNull($firstHash->Hash);
898
    }
899
900
    public function testRememberMeHashAutologin()
901
    {
902
        /**
903
 * @var Member $m1
904
*/
905
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
906
907
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
908
909
        /** @var RememberLoginHash $firstHash */
910
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
911
        $this->assertNotNull($firstHash);
912
913
        // re-generates the hash so we can get the token
914
        $firstHash->Hash = $firstHash->getNewHash($m1);
915
        $token = $firstHash->getToken();
916
        $firstHash->write();
917
918
        $response = $this->get(
919
            'Security/login',
920
            $this->session(),
921
            null,
922
            array(
923
                'alc_enc' => $m1->ID.':'.$token,
924
                'alc_device' => $firstHash->DeviceID
925
            )
926
        );
927
        $message = Convert::raw2xml(
928
            _t(
929
                'SilverStripe\\Security\\Member.LOGGEDINAS',
930
                "You're logged in as {name}.",
931
                array('name' => $m1->FirstName)
932
            )
933
        );
934
        $this->assertContains($message, $response->getBody());
935
936
        $this->logOut();
937
938
        // A wrong token or a wrong device ID should not let us autologin
939
        $response = $this->get(
940
            'Security/login',
941
            $this->session(),
942
            null,
943
            array(
944
                'alc_enc' => $m1->ID.':asdfasd'.str_rot13($token),
945
                'alc_device' => $firstHash->DeviceID
946
            )
947
        );
948
        $this->assertNotContains($message, $response->getBody());
949
950
        $response = $this->get(
951
            'Security/login',
952
            $this->session(),
953
            null,
954
            array(
955
                'alc_enc' => $m1->ID.':'.$token,
956
                'alc_device' => str_rot13($firstHash->DeviceID)
957
            )
958
        );
959
        $this->assertNotContains($message, $response->getBody());
960
961
        // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
962
        // should remove all previous hashes for this device
963
        $response = $this->post(
964
            'Security/login/default/LoginForm',
965
            array(
966
                'Email' => $m1->Email,
967
                'Password' => '1nitialPassword',
968
                'action_doLogin' => 'action_doLogin'
969
            ),
970
            null,
971
            $this->session(),
972
            null,
973
            array(
974
                'alc_device' => $firstHash->DeviceID
975
            )
976
        );
977
        $this->assertContains($message, $response->getBody());
978
        $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
979
    }
980
981
    public function testExpiredRememberMeHashAutologin()
982
    {
983
        /** @var Member $m1 */
984
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
985
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
986
        /** @var RememberLoginHash $firstHash */
987
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
988
        $this->assertNotNull($firstHash);
989
990
        // re-generates the hash so we can get the token
991
        $firstHash->Hash = $firstHash->getNewHash($m1);
992
        $token = $firstHash->getToken();
993
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
994
        $firstHash->write();
995
996
        DBDatetime::set_mock_now('1999-12-31 23:59:59');
997
998
        $response = $this->get(
999
            'Security/login',
1000
            $this->session(),
1001
            null,
1002
            array(
1003
                'alc_enc' => $m1->ID.':'.$token,
1004
                'alc_device' => $firstHash->DeviceID
1005
            )
1006
        );
1007
        $message = Convert::raw2xml(
1008
            _t(
1009
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1010
                "You're logged in as {name}.",
1011
                array('name' => $m1->FirstName)
1012
            )
1013
        );
1014
        $this->assertContains($message, $response->getBody());
1015
1016
        $this->logOut();
1017
1018
        // re-generates the hash so we can get the token
1019
        $firstHash->Hash = $firstHash->getNewHash($m1);
1020
        $token = $firstHash->getToken();
1021
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1022
        $firstHash->write();
1023
1024
        DBDatetime::set_mock_now('2000-01-01 00:00:01');
1025
1026
        $response = $this->get(
1027
            'Security/login',
1028
            $this->session(),
1029
            null,
1030
            array(
1031
                'alc_enc' => $m1->ID.':'.$token,
1032
                'alc_device' => $firstHash->DeviceID
1033
            )
1034
        );
1035
        $this->assertNotContains($message, $response->getBody());
1036
        $this->logOut();
1037
        DBDatetime::clear_mock_now();
1038
    }
1039
1040
    public function testRememberMeMultipleDevices()
1041
    {
1042
        /** @var Member $m1 */
1043
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1044
1045
        // First device
1046
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1047
        Cookie::set('alc_device', null);
1048
        // Second device
1049
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1050
1051
        // Hash of first device
1052
        /** @var RememberLoginHash $firstHash */
1053
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1054
        $this->assertNotNull($firstHash);
1055
1056
        // Hash of second device
1057
        /** @var RememberLoginHash $secondHash */
1058
        $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
1059
        $this->assertNotNull($secondHash);
1060
1061
        // DeviceIDs are different
1062
        $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1063
1064
        // re-generates the hashes so we can get the tokens
1065
        $firstHash->Hash = $firstHash->getNewHash($m1);
1066
        $firstToken = $firstHash->getToken();
1067
        $firstHash->write();
1068
1069
        $secondHash->Hash = $secondHash->getNewHash($m1);
1070
        $secondToken = $secondHash->getToken();
1071
        $secondHash->write();
1072
1073
        // Accessing the login page should show the user's name straight away
1074
        $response = $this->get(
1075
            'Security/login',
1076
            $this->session(),
1077
            null,
1078
            array(
1079
                'alc_enc' => $m1->ID.':'.$firstToken,
1080
                'alc_device' => $firstHash->DeviceID
1081
            )
1082
        );
1083
        $message = Convert::raw2xml(
1084
            _t(
1085
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1086
                "You're logged in as {name}.",
1087
                array('name' => $m1->FirstName)
1088
            )
1089
        );
1090
        $this->assertContains($message, $response->getBody());
1091
1092
        // Test that removing session but not cookie keeps user
1093
        /** @var SessionAuthenticationHandler $sessionHandler */
1094
        $sessionHandler = Injector::inst()->get(SessionAuthenticationHandler::class);
1095
        $sessionHandler->logOut();
1096
        Security::setCurrentUser(null);
1097
1098
        // Accessing the login page from the second device
1099
        $response = $this->get(
1100
            'Security/login',
1101
            $this->session(),
1102
            null,
1103
            array(
1104
                'alc_enc' => $m1->ID.':'.$secondToken,
1105
                'alc_device' => $secondHash->DeviceID
1106
            )
1107
        );
1108
        $this->assertContains($message, $response->getBody());
1109
1110
        // Logging out from the second device - only one device being logged out
1111
        RememberLoginHash::config()->update('logout_across_devices', false);
1112
        $this->get(
1113
            'Security/logout',
1114
            $this->session(),
1115
            null,
1116
            array(
1117
                'alc_enc' => $m1->ID.':'.$secondToken,
1118
                'alc_device' => $secondHash->DeviceID
1119
            )
1120
        );
1121
        $this->assertEquals(
1122
            RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1123
            1
1124
        );
1125
1126
        // Logging out from any device when all login hashes should be removed
1127
        RememberLoginHash::config()->update('logout_across_devices', true);
1128
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1129
        $this->get('Security/logout', $this->session());
1130
        $this->assertEquals(
1131
            RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1132
            0
1133
        );
1134
    }
1135
1136
    public function testCanDelete()
1137
    {
1138
        $admin1 = $this->objFromFixture(Member::class, 'admin');
1139
        $admin2 = $this->objFromFixture(Member::class, 'other-admin');
1140
        $member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1141
        $member2 = $this->objFromFixture(Member::class, 'noformatmember');
1142
1143
        $this->assertTrue(
1144
            $admin1->canDelete($admin2),
1145
            'Admins can delete other admins'
1146
        );
1147
        $this->assertTrue(
1148
            $member1->canDelete($admin2),
1149
            'Admins can delete non-admins'
1150
        );
1151
        $this->assertFalse(
1152
            $admin1->canDelete($admin1),
1153
            'Admins can not delete themselves'
1154
        );
1155
        $this->assertFalse(
1156
            $member1->canDelete($member2),
1157
            'Non-admins can not delete other non-admins'
1158
        );
1159
        $this->assertFalse(
1160
            $member1->canDelete($member1),
1161
            'Non-admins can not delete themselves'
1162
        );
1163
    }
1164
1165
    public function testFailedLoginCount()
1166
    {
1167
        $maxFailedLoginsAllowed = 3;
1168
        //set up the config variables to enable login lockouts
1169
        Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1170
1171
        /** @var Member $member */
1172
        $member = $this->objFromFixture(Member::class, 'test');
1173
        $failedLoginCount = $member->FailedLoginCount;
1174
1175
        for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1176
            $member->registerFailedLogin();
1177
1178
            $this->assertEquals(
1179
                ++$failedLoginCount,
1180
                $member->FailedLoginCount,
1181
                'Failed to increment $member->FailedLoginCount'
1182
            );
1183
1184
            $this->assertTrue(
1185
                $member->canLogin(),
1186
                "Member has been locked out too early"
1187
            );
1188
        }
1189
    }
1190
1191
    public function testMemberValidator()
1192
    {
1193
        // clear custom requirements for this test
1194
        Member_Validator::config()->update('customRequired', null);
1195
        /** @var Member $memberA */
1196
        $memberA = $this->objFromFixture(Member::class, 'admin');
1197
        /** @var Member $memberB */
1198
        $memberB = $this->objFromFixture(Member::class, 'test');
1199
1200
        // create a blank form
1201
        $form = new MemberTest\ValidatorForm();
1202
1203
        $validator = new Member_Validator();
1204
        $validator->setForm($form);
1205
1206
        // Simulate creation of a new member via form, but use an existing member identifier
1207
        $fail = $validator->php(
1208
            array(
1209
            'FirstName' => 'Test',
1210
            'Email' => $memberA->Email
1211
            )
1212
        );
1213
1214
        $this->assertFalse(
1215
            $fail,
1216
            'Member_Validator must fail when trying to create new Member with existing Email.'
1217
        );
1218
1219
        // populate the form with values from another member
1220
        $form->loadDataFrom($memberB);
1221
1222
        // Assign the validator to an existing member
1223
        // (this is basically the same as passing the member ID with the form data)
1224
        $validator->setForMember($memberB);
1225
1226
        // Simulate update of a member via form and use an existing member Email
1227
        $fail = $validator->php(
1228
            array(
1229
            'FirstName' => 'Test',
1230
            'Email' => $memberA->Email
1231
            )
1232
        );
1233
1234
        // Simulate update to a new Email address
1235
        $pass1 = $validator->php(
1236
            array(
1237
            'FirstName' => 'Test',
1238
            'Email' => '[email protected]'
1239
            )
1240
        );
1241
1242
        // Pass in the same Email address that the member already has. Ensure that case is valid
1243
        $pass2 = $validator->php(
1244
            array(
1245
            'FirstName' => 'Test',
1246
            'Surname' => 'User',
1247
            'Email' => $memberB->Email
1248
            )
1249
        );
1250
1251
        $this->assertFalse(
1252
            $fail,
1253
            'Member_Validator must fail when trying to update existing member with existing Email.'
1254
        );
1255
1256
        $this->assertTrue(
1257
            $pass1,
1258
            'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1259
        );
1260
1261
        $this->assertTrue(
1262
            $pass2,
1263
            'Member_Validator must pass when Member updates his own Email to the already existing value.'
1264
        );
1265
    }
1266
1267
    public function testMemberValidatorWithExtensions()
1268
    {
1269
        // clear custom requirements for this test
1270
        Member_Validator::config()->update('customRequired', null);
1271
1272
        // create a blank form
1273
        $form = new MemberTest\ValidatorForm();
1274
1275
        // Test extensions
1276
        Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1277
        $validator = new Member_Validator();
1278
        $validator->setForm($form);
1279
1280
        // This test should fail, since the extension enforces FirstName == Surname
1281
        $fail = $validator->php(
1282
            array(
1283
            'FirstName' => 'Test',
1284
            'Surname' => 'User',
1285
            'Email' => '[email protected]'
1286
            )
1287
        );
1288
1289
        $pass = $validator->php(
1290
            array(
1291
            'FirstName' => 'Test',
1292
            'Surname' => 'Test',
1293
            'Email' => '[email protected]'
1294
            )
1295
        );
1296
1297
        $this->assertFalse(
1298
            $fail,
1299
            'Member_Validator must fail because of added extension.'
1300
        );
1301
1302
        $this->assertTrue(
1303
            $pass,
1304
            'Member_Validator must succeed, since it meets all requirements.'
1305
        );
1306
1307
        // Add another extension that always fails. This ensures that all extensions are considered in the validation
1308
        Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1309
        $validator = new Member_Validator();
1310
        $validator->setForm($form);
1311
1312
        // Even though the data is valid, This test should still fail, since one extension always returns false
1313
        $fail = $validator->php(
1314
            array(
1315
            'FirstName' => 'Test',
1316
            'Surname' => 'Test',
1317
            'Email' => '[email protected]'
1318
            )
1319
        );
1320
1321
        $this->assertFalse(
1322
            $fail,
1323
            'Member_Validator must fail because of added extensions.'
1324
        );
1325
1326
        // Remove added extensions
1327
        Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1328
        Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1329
    }
1330
1331
    public function testCustomMemberValidator()
1332
    {
1333
        // clear custom requirements for this test
1334
        Member_Validator::config()->update('customRequired', null);
1335
1336
        $member = $this->objFromFixture(Member::class, 'admin');
1337
1338
        $form = new MemberTest\ValidatorForm();
1339
        $form->loadDataFrom($member);
1340
1341
        $validator = new Member_Validator();
1342
        $validator->setForm($form);
1343
1344
        $pass = $validator->php(
1345
            array(
1346
            'FirstName' => 'Borris',
1347
            'Email' => '[email protected]'
1348
            )
1349
        );
1350
1351
        $fail = $validator->php(
1352
            array(
1353
            'Email' => '[email protected]',
1354
            'Surname' => ''
1355
            )
1356
        );
1357
1358
        $this->assertTrue($pass, 'Validator requires a FirstName and Email');
1359
        $this->assertFalse($fail, 'Missing FirstName');
1360
1361
        $ext = new MemberTest\ValidatorExtension();
1362
        $ext->updateValidator($validator);
1363
1364
        $pass = $validator->php(
1365
            array(
1366
            'FirstName' => 'Borris',
1367
            'Email' => '[email protected]'
1368
            )
1369
        );
1370
1371
        $fail = $validator->php(
1372
            array(
1373
            'Email' => '[email protected]'
1374
            )
1375
        );
1376
1377
        $this->assertFalse($pass, 'Missing surname');
1378
        $this->assertFalse($fail, 'Missing surname value');
1379
1380
        $fail = $validator->php(
1381
            array(
1382
            'Email' => '[email protected]',
1383
            'Surname' => 'Silverman'
1384
            )
1385
        );
1386
1387
        $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1388
    }
1389
1390
    public function testCurrentUser()
1391
    {
1392
        $this->assertNull(Security::getCurrentUser());
1393
1394
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1395
        $this->logInAs($adminMember);
1396
1397
        $userFromSession = Security::getCurrentUser();
1398
        $this->assertEquals($adminMember->ID, $userFromSession->ID);
1399
    }
1400
1401
    /**
1402
     * @covers \SilverStripe\Security\Member::actAs()
1403
     */
1404
    public function testActAsUserPermissions()
1405
    {
1406
        $this->assertNull(Security::getCurrentUser());
1407
1408
        /** @var Member $adminMember */
1409
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1410
1411
        // Check acting as admin when not logged in
1412
        $checkAdmin = Member::actAs($adminMember, function () {
1413
            return Permission::check('ADMIN');
1414
        });
1415
        $this->assertTrue($checkAdmin);
1416
1417
        // Check nesting
1418
        $checkAdmin = Member::actAs($adminMember, function () {
1419
            return Member::actAs(null, function () {
1420
                return Permission::check('ADMIN');
1421
            });
1422
        });
1423
        $this->assertFalse($checkAdmin);
1424
1425
        // Check logging in as non-admin user
1426
        $this->logInWithPermission('TEST_PERMISSION');
1427
1428
        $hasPerm = Member::actAs(null, function () {
1429
            return Permission::check('TEST_PERMISSION');
1430
        });
1431
        $this->assertFalse($hasPerm);
1432
1433
        // Check permissions can be promoted
1434
        $checkAdmin = Member::actAs($adminMember, function () {
1435
            return Permission::check('ADMIN');
1436
        });
1437
        $this->assertTrue($checkAdmin);
1438
    }
1439
1440
    /**
1441
     * @covers \SilverStripe\Security\Member::actAs()
1442
     */
1443
    public function testActAsUser()
1444
    {
1445
        $this->assertNull(Security::getCurrentUser());
1446
1447
        /** @var Member $adminMember */
1448
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1449
        $member = Member::actAs($adminMember, function () {
1450
            return Security::getCurrentUser();
1451
        });
1452
        $this->assertEquals($adminMember->ID, $member->ID);
1453
1454
        // Check nesting
1455
        $member = Member::actAs($adminMember, function () {
1456
            return Member::actAs(null, function () {
1457
                return Security::getCurrentUser();
1458
            });
1459
        });
1460
        $this->assertEmpty($member);
1461
    }
1462
1463
    public function testChangePasswordWithExtensionsThatModifyValidationResult()
1464
    {
1465
        // Default behaviour
1466
        $member = $this->objFromFixture(Member::class, 'admin');
1467
        $result = $member->changePassword('my-secret-new-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

1467
        /** @scrutinizer ignore-call */ 
1468
        $result = $member->changePassword('my-secret-new-password');
Loading history...
1468
        $this->assertInstanceOf(ValidationResult::class, $result);
1469
        $this->assertTrue($result->isValid());
1470
1471
        // With an extension added
1472
        Member::add_extension(MemberTest\ExtendedChangePasswordExtension::class);
1473
        $member = $this->objFromFixture(Member::class, 'admin');
1474
        $result = $member->changePassword('my-second-secret-password');
1475
        $this->assertInstanceOf(ValidationResult::class, $result);
1476
        $this->assertFalse($result->isValid());
1477
    }
1478
}
1479