Passed
Push — 4.1 ( dd3fbf...7ec5fa )
by Daniel
11:01
created

MemberTest::testRemoveGroups()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 0
dl 0
loc 23
rs 9.0856
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->assertEquals(
119
            Security::config()->get('password_encryption_algorithm'),
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
    /**
415
     * Assertions to check that Member_GroupSet is functionally equivalent to ManyManyList
416
     */
417
    public function testRemoveGroups()
418
    {
419
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
420
421
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
422
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
423
424
        $this->assertTrue(
425
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
0 ignored issues
show
Bug introduced by
The method inGroups() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

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

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

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

1490
        /** @scrutinizer ignore-call */ 
1491
        $result = $member->changePassword('my-secret-new-password');
Loading history...
1491
        $this->assertInstanceOf(ValidationResult::class, $result);
1492
        $this->assertTrue($result->isValid());
1493
1494
        // With an extension added
1495
        Member::add_extension(MemberTest\ExtendedChangePasswordExtension::class);
1496
        $member = $this->objFromFixture(Member::class, 'admin');
1497
        $result = $member->changePassword('my-second-secret-password');
1498
        $this->assertInstanceOf(ValidationResult::class, $result);
1499
        $this->assertFalse($result->isValid());
1500
    }
1501
}
1502