silverstripe-framework/tests/security/MemberTest.php

1371 lines
42 KiB
PHP
Raw Normal View History

<?php
/**
* @package framework
* @subpackage tests
*/
class MemberTest extends FunctionalTest {
protected static $fixture_file = 'MemberTest.yml';
2014-08-15 08:53:05 +02:00
protected $orig = array();
2014-08-15 08:53:05 +02:00
protected $local = null;
protected $illegalExtensions = array(
'Member' => array(
// TODO Coupling with modules, this should be resolved by automatically
// removing all applied extensions before a unit test
'ForumRole',
'OpenIDAuthenticatedRole'
)
);
public function __construct() {
parent::__construct();
//Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
//This is because the test relies on the yaml file being interpreted according to a particular date format
2014-08-15 08:53:05 +02:00
//and this setup occurs before the setUp method is run
$this->local = i18n::default_locale();
i18n::set_default_locale('en_US');
}
public function __destruct() {
i18n::set_default_locale($this->local);
}
public function setUp() {
parent::setUp();
2014-08-15 08:53:05 +02:00
$this->orig['Member_unique_identifier_field'] = Member::config()->unique_identifier_field;
Member::config()->unique_identifier_field = 'Email';
Member::set_password_validator(null);
}
2014-08-15 08:53:05 +02:00
public function tearDown() {
Member::config()->unique_identifier_field = $this->orig['Member_unique_identifier_field'];
parent::tearDown();
}
/**
* @expectedException ValidationException
*/
public function testWriteDoesntMergeNewRecordWithExistingMember() {
$m1 = new Member();
$m1->Email = 'member@test.com';
$m1->write();
2014-08-15 08:53:05 +02:00
$m2 = new Member();
$m2->Email = 'member@test.com';
$m2->write();
}
2014-08-15 08:53:05 +02:00
/**
* @expectedException ValidationException
*/
public function testWriteDoesntMergeExistingMemberOnIdentifierChange() {
$m1 = new Member();
$m1->Email = 'member@test.com';
$m1->write();
2014-08-15 08:53:05 +02:00
$m2 = new Member();
$m2->Email = 'member_new@test.com';
$m2->write();
2014-08-15 08:53:05 +02:00
$m2->Email = 'member@test.com';
$m2->write();
}
2014-08-15 08:53:05 +02:00
public function testDefaultPasswordEncryptionOnMember() {
$memberWithPassword = new Member();
$memberWithPassword->Password = 'mypassword';
$memberWithPassword->write();
$this->assertEquals(
2014-08-15 08:53:05 +02:00
$memberWithPassword->PasswordEncryption,
Security::config()->password_encryption_algorithm,
'Password encryption is set for new member records on first write (with setting "Password")'
);
2014-08-15 08:53:05 +02:00
$memberNoPassword = new Member();
$memberNoPassword->write();
$this->assertNull(
$memberNoPassword->PasswordEncryption,
'Password encryption is not set for new member records on first write, when not setting a "Password")'
);
}
2014-08-15 08:53:05 +02:00
public function testDefaultPasswordEncryptionDoesntChangeExistingMembers() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
2014-08-15 08:53:05 +02:00
$origAlgo = Security::config()->password_encryption_algorithm;
Security::config()->password_encryption_algorithm = 'none';
2014-08-15 08:53:05 +02:00
$member->Password = 'mynewpassword';
$member->write();
2014-08-15 08:53:05 +02:00
$this->assertEquals(
2014-08-15 08:53:05 +02:00
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('mynewpassword');
$this->assertTrue($result->valid());
2014-08-15 08:53:05 +02:00
Security::config()->password_encryption_algorithm = $origAlgo;
}
public function testKeepsEncryptionOnEmptyPasswords() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
2014-08-15 08:53:05 +02:00
$member->Password = '';
$member->write();
2014-08-15 08:53:05 +02:00
$this->assertEquals(
2014-08-15 08:53:05 +02:00
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('');
$this->assertTrue($result->valid());
}
2014-08-15 08:53:05 +02:00
public function testSetPassword() {
$member = $this->objFromFixture('Member', 'test');
$member->Password = "test1";
$member->write();
$result = $member->checkPassword('test1');
$this->assertTrue($result->valid());
}
2014-08-15 08:53:05 +02:00
/**
* Test that password changes are logged properly
*/
public function testPasswordChangeLogging() {
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$member->Password = "test1";
$member->write();
2014-08-15 08:53:05 +02:00
$member->Password = "test2";
$member->write();
2014-08-15 08:53:05 +02:00
$member->Password = "test3";
$member->write();
2014-08-15 08:53:05 +02:00
$passwords = DataObject::get("MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
->getIterator();
$this->assertNotNull($passwords);
$passwords->rewind();
$this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
2014-08-15 08:53:05 +02:00
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
2014-08-15 08:53:05 +02:00
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
2014-08-15 08:53:05 +02:00
$passwords->next();
$this->assertInstanceOf('DataObject', $passwords->current());
$this->assertTrue($passwords->current()->checkPassword('1nitialPassword'),
"Password 1nitialPassword not found in MemberRecord");
//check we don't retain orphaned records when a member is deleted
$member->delete();
$passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
$this->assertCount(0, $passwords);
}
2014-08-15 08:53:05 +02:00
/**
* Test that changed passwords will send an email
*/
public function testChangedPasswordEmaling() {
$this->clearEmails();
2014-08-15 08:53:05 +02:00
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$valid = $member->changePassword('32asDF##$$%%');
$this->assertTrue($valid->valid());
/*
$this->assertEmailSent("sam@silverstripe.com", null, "/changed password/",
'/sam@silverstripe\.com.*32asDF##\$\$%%/');
*/
}
2014-08-15 08:53:05 +02:00
/**
* Test that passwords validate against NZ e-government guidelines
* - don't allow the use of the last 6 passwords
* - require at least 3 of lowercase, uppercase, digits and punctuation
* - at least 7 characters long
*/
public function testValidatePassword() {
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
2014-08-15 08:53:05 +02:00
Member::set_password_validator(new MemberTest_PasswordValidator());
2014-08-15 08:53:05 +02:00
// BAD PASSWORDS
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('shorty');
$this->assertFalse($valid->valid());
$this->assertContains("TOO_SHORT", $valid->codeList());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('longone');
$this->assertNotContains("TOO_SHORT", $valid->codeList());
$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertFalse($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('w1thNumb3rs');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
// Clear out the MemberPassword table to ensure that the system functions properly in that situation
DB::query("DELETE FROM \"MemberPassword\"");
2014-08-15 08:53:05 +02:00
// GOOD PASSWORDS
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls5');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls6');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls7');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
// CAN'T USE PASSWORDS 2-7, but I can use pasword 1
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls2');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls5');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls7');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
// HAVING DONE THAT, PASSWORD 2 is now available from the list
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
Member::set_password_validator(null);
}
2014-08-15 08:53:05 +02:00
/**
* Test that the PasswordExpiry date is set when passwords are changed
*/
public function testPasswordExpirySetting() {
Member::config()->password_expiry_days = 90;
2014-08-15 08:53:05 +02:00
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$valid = $member->changePassword("Xx?1234234");
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$expiryDate = date('Y-m-d', time() + 90*86400);
$this->assertEquals($expiryDate, $member->PasswordExpiry);
2014-08-15 08:53:05 +02:00
Member::config()->password_expiry_days = null;
$valid = $member->changePassword("Xx?1234235");
$this->assertTrue($valid->valid());
2014-08-15 08:53:05 +02:00
$this->assertNull($member->PasswordExpiry);
}
2014-08-15 08:53:05 +02:00
public function testIsPasswordExpired() {
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$this->assertFalse($member->isPasswordExpired());
2014-08-15 08:53:05 +02:00
$member = $this->objFromFixture('Member', 'noexpiry');
$member->PasswordExpiry = null;
$this->assertFalse($member->isPasswordExpired());
2014-08-15 08:53:05 +02:00
$member = $this->objFromFixture('Member', 'expiredpassword');
$this->assertTrue($member->isPasswordExpired());
2014-08-15 08:53:05 +02:00
// Check the boundary conditions
// If PasswordExpiry == today, then it's expired
$member->PasswordExpiry = date('Y-m-d');
$this->assertTrue($member->isPasswordExpired());
2014-08-15 08:53:05 +02:00
// If PasswordExpiry == tomorrow, then it's not
$member->PasswordExpiry = date('Y-m-d', time() + 86400);
$this->assertFalse($member->isPasswordExpired());
2014-08-15 08:53:05 +02:00
}
2014-08-15 08:53:05 +02:00
public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() {
Config::inst()->update('i18n', 'date_format', 'yyyy-MM-dd');
Config::inst()->update('i18n', 'time_format', 'H:mm');
$member = $this->objFromFixture('Member', 'noformatmember');
$this->assertEquals('yyyy-MM-dd', $member->DateFormat);
$this->assertEquals('H:mm', $member->TimeFormat);
}
2014-08-15 08:53:05 +02:00
public function testInGroups() {
$staffmember = $this->objFromFixture('Member', 'staffmember');
$managementmember = $this->objFromFixture('Member', 'managementmember');
$accountingmember = $this->objFromFixture('Member', 'accountingmember');
$ceomember = $this->objFromFixture('Member', 'ceomember');
2014-08-15 08:53:05 +02:00
$staffgroup = $this->objFromFixture('Group', 'staffgroup');
$managementgroup = $this->objFromFixture('Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('Group', 'ceogroup');
2014-08-15 08:53:05 +02:00
$this->assertTrue(
$staffmember->inGroups(array($staffgroup, $managementgroup)),
'inGroups() succeeds if a membership is detected on one of many passed groups'
);
$this->assertFalse(
$staffmember->inGroups(array($ceogroup, $managementgroup)),
'inGroups() fails if a membership is detected on none of the passed groups'
);
$this->assertFalse(
$ceomember->inGroups(array($staffgroup, $managementgroup), true),
'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
);
}
2014-08-15 08:53:05 +02:00
public function testAddToGroupByCode() {
$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
2014-08-15 08:53:05 +02:00
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
2014-08-15 08:53:05 +02:00
$grouplessMember->addToGroupByCode('memberless');
2014-08-15 08:53:05 +02:00
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
2014-08-15 08:53:05 +02:00
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
2014-08-15 08:53:05 +02:00
$group = DataObject::get_one('Group', array(
'"Group"."Code"' => 'somegroupthatwouldneverexist'
));
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
2014-08-15 08:53:05 +02:00
}
2014-08-15 08:53:05 +02:00
public function testRemoveFromGroupByCode() {
$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
2014-08-15 08:53:05 +02:00
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
2014-08-15 08:53:05 +02:00
$grouplessMember->addToGroupByCode('memberless');
2014-08-15 08:53:05 +02:00
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
2014-08-15 08:53:05 +02:00
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
2014-08-15 08:53:05 +02:00
$group = DataObject::get_one('Group', "\"Code\" = 'somegroupthatwouldneverexist'");
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
2014-08-15 08:53:05 +02:00
$grouplessMember->removeFromGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 0);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
2014-08-15 08:53:05 +02:00
$grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
$this->assertEquals($grouplessMember->Groups()->Count(), 0);
}
2014-08-15 08:53:05 +02:00
public function testInGroup() {
$staffmember = $this->objFromFixture('Member', 'staffmember');
$managementmember = $this->objFromFixture('Member', 'managementmember');
$accountingmember = $this->objFromFixture('Member', 'accountingmember');
$ceomember = $this->objFromFixture('Member', 'ceomember');
2014-08-15 08:53:05 +02:00
$staffgroup = $this->objFromFixture('Group', 'staffgroup');
$managementgroup = $this->objFromFixture('Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('Group', 'ceogroup');
2014-08-15 08:53:05 +02:00
$this->assertTrue(
$staffmember->inGroup($staffgroup),
'Direct group membership is detected'
);
$this->assertTrue(
$managementmember->inGroup($staffgroup),
'Users of child group are members of a direct parent group (if not in strict mode)'
);
$this->assertTrue(
$accountingmember->inGroup($staffgroup),
'Users of child group are members of a direct parent group (if not in strict mode)'
);
$this->assertTrue(
$ceomember->inGroup($staffgroup),
'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
);
$this->assertTrue(
$ceomember->inGroup($ceogroup, true),
'Direct group membership is dected (if in strict mode)'
);
$this->assertFalse(
$ceomember->inGroup($staffgroup, true),
'Users of child group are not members of a direct parent group (if in strict mode)'
);
$this->assertFalse(
$staffmember->inGroup($managementgroup),
'Users of parent group are not members of a direct child group'
);
$this->assertFalse(
$staffmember->inGroup($ceogroup),
'Users of parent group are not members of an indirect grandchild group'
);
$this->assertFalse(
$accountingmember->inGroup($managementgroup),
'Users of group are not members of any siblings'
);
$this->assertFalse(
$staffmember->inGroup('does-not-exist'),
'Non-existant group returns false'
);
}
2014-08-15 08:53:05 +02:00
/**
* Tests that the user is able to view their own record, and in turn, they can
* edit and delete their own record too.
*/
public function testCanManipulateOwnRecord() {
$extensions = $this->removeExtensions(Object::get_extensions('Member'));
$member = $this->objFromFixture('Member', 'test');
$member2 = $this->objFromFixture('Member', 'staffmember');
2014-08-15 08:53:05 +02:00
$this->session()->inst_set('loggedInAs', null);
2014-08-15 08:53:05 +02:00
/* Not logged in, you can't view, delete or edit the record */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
2014-08-15 08:53:05 +02:00
/* Logged in users can edit their own record */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->assertTrue($member->canView());
$this->assertFalse($member->canDelete());
$this->assertTrue($member->canEdit());
2014-08-15 08:53:05 +02:00
/* Other uses cannot view, delete or edit others records */
$this->session()->inst_set('loggedInAs', $member2->ID);
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
2014-08-15 08:53:05 +02:00
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
2014-08-15 08:53:05 +02:00
public function testAuthorisedMembersCanManipulateOthersRecords() {
$extensions = $this->removeExtensions(Object::get_extensions('Member'));
$member = $this->objFromFixture('Member', 'test');
$member2 = $this->objFromFixture('Member', 'staffmember');
2014-08-15 08:53:05 +02:00
/* Group members with SecurityAdmin permissions can manipulate other records */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->assertTrue($member2->canView());
$this->assertTrue($member2->canDelete());
$this->assertTrue($member2->canEdit());
2014-08-15 08:53:05 +02:00
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
2014-08-15 08:53:05 +02:00
public function testExtendedCan() {
$extensions = $this->removeExtensions(Object::get_extensions('Member'));
$member = $this->objFromFixture('Member', 'test');
2014-08-15 08:53:05 +02:00
/* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
2014-08-15 08:53:05 +02:00
/* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
Member::add_extension('MemberTest_ViewingAllowedExtension');
$member2 = $this->objFromFixture('Member', 'staffmember');
2014-08-15 08:53:05 +02:00
$this->assertTrue($member2->canView());
$this->assertFalse($member2->canDelete());
$this->assertFalse($member2->canEdit());
2014-08-15 08:53:05 +02:00
/* Apply a extension that denies viewing of the Member */
Member::remove_extension('MemberTest_ViewingAllowedExtension');
Member::add_extension('MemberTest_ViewingDeniedExtension');
$member3 = $this->objFromFixture('Member', 'managementmember');
2014-08-15 08:53:05 +02:00
$this->assertFalse($member3->canView());
$this->assertFalse($member3->canDelete());
$this->assertFalse($member3->canEdit());
2014-08-15 08:53:05 +02:00
/* Apply a extension that allows viewing and editing but denies deletion */
Member::remove_extension('MemberTest_ViewingDeniedExtension');
Member::add_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
$member4 = $this->objFromFixture('Member', 'accountingmember');
2014-08-15 08:53:05 +02:00
$this->assertTrue($member4->canView());
$this->assertFalse($member4->canDelete());
$this->assertTrue($member4->canEdit());
2014-08-15 08:53:05 +02:00
Member::remove_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
$this->addExtensions($extensions);
}
2014-08-15 08:53:05 +02:00
/**
* Tests for {@link Member::getName()} and {@link Member::setName()}
*/
public function testName() {
$member = $this->objFromFixture('Member', 'test');
$member->setName('Test Some User');
$this->assertEquals('Test Some User', $member->getName());
$member->setName('Test');
$this->assertEquals('Test', $member->getName());
$member->FirstName = 'Test';
$member->Surname = '';
$this->assertEquals('Test', $member->getName());
}
2014-08-15 08:53:05 +02:00
public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() {
$adminMember = $this->objFromFixture('Member', 'admin');
$otherAdminMember = $this->objFromFixture('Member', 'other-admin');
$securityAdminMember = $this->objFromFixture('Member', 'test');
$ceoMember = $this->objFromFixture('Member', 'ceomember');
2014-08-15 08:53:05 +02:00
// Careful: Don't read as english language.
// More precisely this should read canBeEditedBy()
2014-08-15 08:53:05 +02:00
$this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
$this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
$this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
2014-08-15 08:53:05 +02:00
$this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
$this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
$this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
}
2014-08-15 08:53:05 +02:00
public function testOnChangeGroups() {
$staffGroup = $this->objFromFixture('Group', 'staffgroup');
$staffMember = $this->objFromFixture('Member', 'staffmember');
$adminMember = $this->objFromFixture('Member', 'admin');
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
$newOtherGroup = new Group(array('Title' => 'othergroup'));
$newOtherGroup->write();
2014-08-15 08:53:05 +02:00
$this->assertTrue(
$staffMember->onChangeGroups(array($staffGroup->ID)),
'Adding existing non-admin group relation is allowed for non-admin members'
);
$this->assertTrue(
$staffMember->onChangeGroups(array($newOtherGroup->ID)),
'Adding new non-admin group relation is allowed for non-admin members'
);
$this->assertFalse(
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is not allowed for non-admin members'
);
$this->session()->inst_set('loggedInAs', $adminMember->ID);
$this->assertTrue(
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
);
$this->session()->inst_set('loggedInAs', null);
$this->assertTrue(
$adminMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for admin members'
);
}
/**
* Test Member_GroupSet::add
*/
public function testOnChangeGroupsByAdd() {
$staffMember = $this->objFromFixture('Member', 'staffmember');
$adminMember = $this->objFromFixture('Member', 'admin');
// Setup new admin group
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
// Setup non-admin group
$newOtherGroup = new Group(array('Title' => 'othergroup'));
$newOtherGroup->write();
// Test staff can be added to other group
$this->assertFalse($staffMember->inGroup($newOtherGroup));
$staffMember->Groups()->add($newOtherGroup);
$this->assertTrue(
$staffMember->inGroup($newOtherGroup),
'Adding new non-admin group relation is allowed for non-admin members'
);
// Test staff member can't be added to admin groups
$this->assertFalse($staffMember->inGroup($newAdminGroup));
$staffMember->Groups()->add($newAdminGroup);
$this->assertFalse(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is not allowed for non-admin members'
);
// Test staff member can be added to admin group by admins
$this->logInAs($adminMember);
$staffMember->Groups()->add($newAdminGroup);
$this->assertTrue(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
);
// Test staff member can be added if they are already admin
$this->session()->inst_set('loggedInAs', null);
$this->assertFalse($adminMember->inGroup($newAdminGroup));
$adminMember->Groups()->add($newAdminGroup);
$this->assertTrue(
$adminMember->inGroup($newAdminGroup),
'Adding new admin group relation is allowed for admin members'
);
}
/**
* Test Member_GroupSet::add
*/
public function testOnChangeGroupsBySetIDList() {
$staffMember = $this->objFromFixture('Member', 'staffmember');
// Setup new admin group
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
// Test staff member can't be added to admin groups
$this->assertFalse($staffMember->inGroup($newAdminGroup));
$staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
$this->assertFalse(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is not allowed for non-admin members'
);
}
/**
* Test that extensions using updateCMSFields() are applied correctly
*/
public function testUpdateCMSFields() {
Member::add_extension('MemberTest_FieldsExtension');
$member = singleton('Member');
$fields = $member->getCMSFields();
$this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
$this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
$this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
Member::remove_extension('MemberTest_FieldsExtension');
}
2014-08-15 08:53:05 +02:00
/**
* Test that all members are returned
*/
public function testMap_in_groupsReturnsAll() {
$members = Member::map_in_groups();
$this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
}
2014-08-15 08:53:05 +02:00
/**
2014-08-15 08:53:05 +02:00
* Test that only admin members are returned
*/
public function testMap_in_groupsReturnsAdmins() {
$adminID = $this->objFromFixture('Group', 'admingroup')->ID;
$members = Member::map_in_groups($adminID)->toArray();
2014-08-15 08:53:05 +02:00
$admin = $this->objFromFixture('Member', 'admin');
$otherAdmin = $this->objFromFixture('Member', 'other-admin');
2014-08-15 08:53:05 +02:00
$this->assertTrue(in_array($admin->getTitle(), $members),
$admin->getTitle().' should be in the returned list.');
$this->assertTrue(in_array($otherAdmin->getTitle(), $members),
$otherAdmin->getTitle().' should be in the returned list.');
$this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
}
/**
* Add the given array of member extensions as class names.
* This is useful for re-adding extensions after being removed
* in a test case to produce an unbiased test.
2014-08-15 08:53:05 +02:00
*
* @param array $extensions
* @return array The added extensions
*/
protected function addExtensions($extensions) {
if($extensions) foreach($extensions as $extension) {
Member::add_extension($extension);
}
return $extensions;
}
/**
* Remove given extensions from Member. This is useful for
* removing extensions that could produce a biased
* test result, as some extensions applied by project
* code or modules can do this.
2014-08-15 08:53:05 +02:00
*
* @param array $extensions
* @return array The removed extensions
*/
protected function removeExtensions($extensions) {
if($extensions) foreach($extensions as $extension) {
Member::remove_extension($extension);
}
return $extensions;
}
public function testGenerateAutologinTokenAndStoreHash() {
$enc = new PasswordEncryptor_Blowfish();
$m = new Member();
$m->PasswordEncryption = 'blowfish';
$m->Salt = $enc->salt('123');
$token = $m->generateAutologinTokenAndStoreHash();
$this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
}
public function testValidateAutoLoginToken() {
$enc = new PasswordEncryptor_Blowfish();
$m1 = new Member();
$m1->PasswordEncryption = 'blowfish';
$m1->Salt = $enc->salt('123');
$m1Token = $m1->generateAutologinTokenAndStoreHash();
$m2 = new Member();
$m2->PasswordEncryption = 'blowfish';
$m2->Salt = $enc->salt('456');
$m2Token = $m2->generateAutologinTokenAndStoreHash();
2014-08-15 08:53:05 +02:00
$this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
$this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
}
public function testRememberMeHashGeneration() {
$m1 = $this->objFromFixture('Member', 'grouplessmember');
$m1->login(true);
$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
$this->assertEquals($hashes->Count(), 1);
$firstHash = $hashes->First();
$this->assertNotNull($firstHash->DeviceID);
$this->assertNotNull($firstHash->Hash);
}
public function testRememberMeHashAutologin() {
$m1 = $this->objFromFixture('Member', 'noexpiry');
$m1->login(true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->write();
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
2016-03-08 21:50:18 +01:00
// A wrong token or a wrong device ID should not let us autologin
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.str_rot13($token),
'alc_device' => $firstHash->DeviceID
)
);
$this->assertNotContains($message, $response->getBody());
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => str_rot13($firstHash->DeviceID)
)
);
$this->assertNotContains($message, $response->getBody());
2016-03-08 21:50:18 +01:00
// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
// should remove all previous hashes for this device
$response = $this->post(
2016-03-08 21:50:18 +01:00
'Security/LoginForm',
array(
'Email' => $m1->Email,
'Password' => '1nitialPassword',
'AuthenticationMethod' => 'MemberAuthenticator',
'action_dologin' => 'action_dologin'
),
null,
$this->session(),
2016-03-08 21:50:18 +01:00
null,
array(
'alc_device' => $firstHash->DeviceID
)
);
$this->assertContains($message, $response->getBody());
$this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(), 0);
}
public function testExpiredRememberMeHashAutologin() {
$m1 = $this->objFromFixture('Member', 'noexpiry');
$m1->login(true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->ExpiryDate = '2000-01-01 00:00:00';
$firstHash->write();
SS_DateTime::set_mock_now('1999-12-31 23:59:59');
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->ExpiryDate = '2000-01-01 00:00:00';
$firstHash->write();
SS_DateTime::set_mock_now('2000-01-01 00:00:01');
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$this->assertNotContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
SS_Datetime::clear_mock_now();
}
public function testRememberMeMultipleDevices() {
$m1 = $this->objFromFixture('Member', 'noexpiry');
// First device
$m1->login(true);
Cookie::set('alc_device', null);
// Second device
$m1->login(true);
// Hash of first device
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// Hash of second device
$secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->Last();
$this->assertNotNull($secondHash);
// DeviceIDs are different
$this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
// re-generates the hashes so we can get the tokens
$firstHash->Hash = $firstHash->getNewHash($m1);
$firstToken = $firstHash->getToken();
$firstHash->write();
$secondHash->Hash = $secondHash->getNewHash($m1);
$secondToken = $secondHash->getToken();
$secondHash->write();
// Accessing the login page should show the user's name straight away
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$firstToken,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
// Accessing the login page from the second device
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$secondToken,
'alc_device' => $secondHash->DeviceID
)
);
$this->assertContains($message, $response->getBody());
$logout_across_devices = Config::inst()->get('RememberLoginHash', 'logout_across_devices');
// Logging out from the second device - only one device being logged out
Config::inst()->update('RememberLoginHash', 'logout_across_devices', false);
$response = $this->get(
2016-03-08 21:50:18 +01:00
'Security/logout',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$secondToken,
'alc_device' => $secondHash->DeviceID
)
);
$this->assertEquals(
2016-03-08 21:50:18 +01:00
RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->Count(),
1
);
// Logging out from any device when all login hashes should be removed
Config::inst()->update('RememberLoginHash', 'logout_across_devices', true);
$m1->login(true);
$response = $this->get('Security/logout', $this->session());
$this->assertEquals(
2016-03-08 21:50:18 +01:00
RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(),
0
);
Config::inst()->update('RememberLoginHash', 'logout_across_devices', $logout_across_devices);
}
public function testCanDelete() {
$admin1 = $this->objFromFixture('Member', 'admin');
$admin2 = $this->objFromFixture('Member', 'other-admin');
$member1 = $this->objFromFixture('Member', 'grouplessmember');
$member2 = $this->objFromFixture('Member', 'noformatmember');
$this->assertTrue(
$admin1->canDelete($admin2),
'Admins can delete other admins'
);
$this->assertTrue(
$member1->canDelete($admin2),
'Admins can delete non-admins'
);
$this->assertFalse(
$admin1->canDelete($admin1),
'Admins can not delete themselves'
);
$this->assertFalse(
$member1->canDelete($member2),
'Non-admins can not delete other non-admins'
);
$this->assertFalse(
$member1->canDelete($member1),
'Non-admins can not delete themselves'
);
}
public function testFailedLoginCount() {
$maxFailedLoginsAllowed = 3;
//set up the config variables to enable login lockouts
Config::nest();
Config::inst()->update('Member', 'lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
$member = $this->objFromFixture('Member', 'test');
$failedLoginCount = $member->FailedLoginCount;
for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
$member->registerFailedLogin();
$this->assertEquals(
++$failedLoginCount,
$member->FailedLoginCount,
'Failed to increment $member->FailedLoginCount'
);
$this->assertFalse(
$member->isLockedOut(),
"Member has been locked out too early"
);
2013-10-23 18:26:05 +02:00
}
}
public function testMemberValidator()
{
// clear custom requirements for this test
Config::inst()->update('Member_Validator', 'customRequired', null);
$memberA = $this->objFromFixture('Member', 'admin');
$memberB = $this->objFromFixture('Member', 'test');
// create a blank form
$form = new MemberTest_ValidatorForm();
$validator = new Member_Validator();
$validator->setForm($form);
// Simulate creation of a new member via form, but use an existing member identifier
$fail = $validator->php(array(
'FirstName' => 'Test',
'Email' => $memberA->Email
));
$this->assertFalse(
$fail,
'Member_Validator must fail when trying to create new Member with existing Email.'
);
// populate the form with values from another member
$form->loadDataFrom($memberB);
// Assign the validator to an existing member
// (this is basically the same as passing the member ID with the form data)
$validator->setForMember($memberB);
// Simulate update of a member via form and use an existing member Email
$fail = $validator->php(array(
'FirstName' => 'Test',
'Email' => $memberA->Email
));
// Simulate update to a new Email address
$pass1 = $validator->php(array(
'FirstName' => 'Test',
'Email' => 'membervalidatortest@testing.com'
));
// Pass in the same Email address that the member already has. Ensure that case is valid
$pass2 = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'User',
'Email' => $memberB->Email
));
$this->assertFalse(
$fail,
'Member_Validator must fail when trying to update existing member with existing Email.'
);
$this->assertTrue(
$pass1,
'Member_Validator must pass when Email is updated to a value that\'s not in use.'
);
$this->assertTrue(
$pass2,
'Member_Validator must pass when Member updates his own Email to the already existing value.'
);
}
public function testMemberValidatorWithExtensions()
{
// clear custom requirements for this test
Config::inst()->update('Member_Validator', 'customRequired', null);
// create a blank form
$form = new MemberTest_ValidatorForm();
// Test extensions
Member_Validator::add_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
$validator = new Member_Validator();
$validator->setForm($form);
// This test should fail, since the extension enforces FirstName == Surname
$fail = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'User',
'Email' => 'test-member-validator-extension@testing.com'
));
$pass = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'Test',
'Email' => 'test-member-validator-extension@testing.com'
));
$this->assertFalse(
$fail,
'Member_Validator must fail because of added extension.'
);
$this->assertTrue(
$pass,
'Member_Validator must succeed, since it meets all requirements.'
);
// Add another extension that always fails. This ensures that all extensions are considered in the validation
Member_Validator::add_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
$validator = new Member_Validator();
$validator->setForm($form);
// Even though the data is valid, This test should still fail, since one extension always returns false
$fail = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'Test',
'Email' => 'test-member-validator-extension@testing.com'
));
$this->assertFalse(
$fail,
'Member_Validator must fail because of added extensions.'
);
// Remove added extensions
Member_Validator::remove_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
Member_Validator::remove_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
}
public function testCustomMemberValidator()
{
// clear custom requirements for this test
Config::inst()->update('Member_Validator', 'customRequired', null);
$member = $this->objFromFixture('Member', 'admin');
$form = new MemberTest_ValidatorForm();
$form->loadDataFrom($member);
$validator = new Member_Validator();
$validator->setForm($form);
$pass = $validator->php(array(
'FirstName' => 'Borris',
'Email' => 'borris@silverstripe.com'
));
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com',
'Surname' => ''
));
$this->assertTrue($pass, 'Validator requires a FirstName and Email');
$this->assertFalse($fail, 'Missing FirstName');
$ext = new MemberTest_ValidatorExtension();
$ext->updateValidator($validator);
$pass = $validator->php(array(
'FirstName' => 'Borris',
'Email' => 'borris@silverstripe.com'
));
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com'
));
$this->assertFalse($pass, 'Missing surname');
$this->assertFalse($fail, 'Missing surname value');
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com',
'Surname' => 'Silverman'
));
$this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ValidatorForm extends Form implements TestOnly {
public function __construct() {
parent::__construct(Controller::curr(), __CLASS__, new FieldList(
new TextField('Email'),
new TextField('Surname'),
new TextField('ID'),
new TextField('FirstName')
), new FieldList(
new FormAction('someAction')
));
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ValidatorExtension extends DataExtension implements TestOnly {
public function updateValidator(&$validator) {
$validator->addRequiredField('Surname');
$validator->removeRequiredField('FirstName');
}
}
/**
* Extension that adds additional validation criteria
* @package framework
* @subpackage tests
*/
class MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension extends DataExtension implements TestOnly
{
public function updatePHP($data, $form) {
return $data['FirstName'] == $data['Surname'];
}
}
/**
* Extension that adds additional validation criteria
* @package framework
* @subpackage tests
*/
class MemberTest_MemberValidator_AlwaysFailsExtension extends DataExtension implements TestOnly
{
public function updatePHP($data, $form) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return true;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_FieldsExtension extends DataExtension implements TestOnly {
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab('Root.Main', new TextField('TestMemberField', 'Test'));
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return true;
}
public function canEdit($member = null) {
return true;
}
public function canDelete($member = null) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_PasswordValidator extends PasswordValidator {
public function __construct() {
parent::__construct();
$this->minLength(7);
$this->checkHistoricalPasswords(6);
$this->characterStrength(3, array('lowercase','uppercase','digits','punctuation'));
}
2014-08-15 08:53:05 +02:00
}