silverstripe-framework/tests/security/MemberTest.php
Jean-Fabien Barrois bab1f230bf NEW Cross device "Remember Me" feature
At the moment, using the "Remember me" function on more than one device will only work with the last device used. Previous devices will not auto login.
This PR introduces a new DataObject for storing hashed tokens against multiple devices. Developers can configure if logging out should discard all tokens, or only the one used on the device logging out; token expiry date is 90 days by default but configurable. For added security, the old behaviour can still be enforced if multiple tokens are not desired.
See silverstripe#1574 for additional background
2016-02-10 09:42:08 +13:00

1219 lines
38 KiB
PHP

<?php
/**
* @package framework
* @subpackage tests
*/
class MemberTest extends FunctionalTest {
protected static $fixture_file = 'MemberTest.yml';
protected $orig = array();
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
//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();
$this->orig['Member_unique_identifier_field'] = Member::config()->unique_identifier_field;
Member::config()->unique_identifier_field = 'Email';
Member::set_password_validator(null);
}
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();
$m2 = new Member();
$m2->Email = 'member@test.com';
$m2->write();
}
/**
* @expectedException ValidationException
*/
public function testWriteDoesntMergeExistingMemberOnIdentifierChange() {
$m1 = new Member();
$m1->Email = 'member@test.com';
$m1->write();
$m2 = new Member();
$m2->Email = 'member_new@test.com';
$m2->write();
$m2->Email = 'member@test.com';
$m2->write();
}
public function testDefaultPasswordEncryptionOnMember() {
$memberWithPassword = new Member();
$memberWithPassword->Password = 'mypassword';
$memberWithPassword->write();
$this->assertEquals(
$memberWithPassword->PasswordEncryption,
Security::config()->password_encryption_algorithm,
'Password encryption is set for new member records on first write (with setting "Password")'
);
$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")'
);
}
public function testDefaultPasswordEncryptionDoesntChangeExistingMembers() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
$origAlgo = Security::config()->password_encryption_algorithm;
Security::config()->password_encryption_algorithm = 'none';
$member->Password = 'mynewpassword';
$member->write();
$this->assertEquals(
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('mynewpassword');
$this->assertTrue($result->valid());
Security::config()->password_encryption_algorithm = $origAlgo;
}
public function testKeepsEncryptionOnEmptyPasswords() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
$member->Password = '';
$member->write();
$this->assertEquals(
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('');
$this->assertTrue($result->valid());
}
public function testSetPassword() {
$member = $this->objFromFixture('Member', 'test');
$member->Password = "test1";
$member->write();
$result = $member->checkPassword('test1');
$this->assertTrue($result->valid());
}
/**
* Test that password changes are logged properly
*/
public function testPasswordChangeLogging() {
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$member->Password = "test1";
$member->write();
$member->Password = "test2";
$member->write();
$member->Password = "test3";
$member->write();
$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");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
$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);
}
/**
* Test that changed passwords will send an email
*/
public function testChangedPasswordEmaling() {
$this->clearEmails();
$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##\$\$%%/');
*/
}
/**
* 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);
Member::set_password_validator(new MemberTest_PasswordValidator());
// BAD PASSWORDS
$valid = $member->changePassword('shorty');
$this->assertFalse($valid->valid());
$this->assertContains("TOO_SHORT", $valid->codeList());
$valid = $member->changePassword('longone');
$this->assertNotContains("TOO_SHORT", $valid->codeList());
$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertFalse($valid->valid());
$valid = $member->changePassword('w1thNumb3rs');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
// Clear out the MemberPassword table to ensure that the system functions properly in that situation
DB::query("DELETE FROM \"MemberPassword\"");
// GOOD PASSWORDS
$valid = $member->changePassword('withSym###Ls');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls5');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls6');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls7');
$this->assertTrue($valid->valid());
// CAN'T USE PASSWORDS 2-7, but I can use pasword 1
$valid = $member->changePassword('withSym###Ls2');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls5');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls7');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls');
$this->assertTrue($valid->valid());
// HAVING DONE THAT, PASSWORD 2 is now available from the list
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
Member::set_password_validator(null);
}
/**
* Test that the PasswordExpiry date is set when passwords are changed
*/
public function testPasswordExpirySetting() {
Member::config()->password_expiry_days = 90;
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$valid = $member->changePassword("Xx?1234234");
$this->assertTrue($valid->valid());
$expiryDate = date('Y-m-d', time() + 90*86400);
$this->assertEquals($expiryDate, $member->PasswordExpiry);
Member::config()->password_expiry_days = null;
$valid = $member->changePassword("Xx?1234235");
$this->assertTrue($valid->valid());
$this->assertNull($member->PasswordExpiry);
}
public function testIsPasswordExpired() {
$member = $this->objFromFixture('Member', 'test');
$this->assertNotNull($member);
$this->assertFalse($member->isPasswordExpired());
$member = $this->objFromFixture('Member', 'noexpiry');
$member->PasswordExpiry = null;
$this->assertFalse($member->isPasswordExpired());
$member = $this->objFromFixture('Member', 'expiredpassword');
$this->assertTrue($member->isPasswordExpired());
// Check the boundary conditions
// If PasswordExpiry == today, then it's expired
$member->PasswordExpiry = date('Y-m-d');
$this->assertTrue($member->isPasswordExpired());
// If PasswordExpiry == tomorrow, then it's not
$member->PasswordExpiry = date('Y-m-d', time() + 86400);
$this->assertFalse($member->isPasswordExpired());
}
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);
}
public function testInGroups() {
$staffmember = $this->objFromFixture('Member', 'staffmember');
$managementmember = $this->objFromFixture('Member', 'managementmember');
$accountingmember = $this->objFromFixture('Member', 'accountingmember');
$ceomember = $this->objFromFixture('Member', 'ceomember');
$staffgroup = $this->objFromFixture('Group', 'staffgroup');
$managementgroup = $this->objFromFixture('Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('Group', 'ceogroup');
$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)'
);
}
public function testAddToGroupByCode() {
$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
$grouplessMember->addToGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
$group = DataObject::get_one('Group', array(
'"Group"."Code"' => 'somegroupthatwouldneverexist'
));
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
}
public function testRemoveFromGroupByCode() {
$grouplessMember = $this->objFromFixture('Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('Group','memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
$grouplessMember->addToGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
$group = DataObject::get_one('Group', "\"Code\" = 'somegroupthatwouldneverexist'");
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
$grouplessMember->removeFromGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 0);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
$this->assertEquals($grouplessMember->Groups()->Count(), 0);
}
public function testInGroup() {
$staffmember = $this->objFromFixture('Member', 'staffmember');
$managementmember = $this->objFromFixture('Member', 'managementmember');
$accountingmember = $this->objFromFixture('Member', 'accountingmember');
$ceomember = $this->objFromFixture('Member', 'ceomember');
$staffgroup = $this->objFromFixture('Group', 'staffgroup');
$managementgroup = $this->objFromFixture('Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('Group', 'ceogroup');
$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'
);
}
/**
* 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');
$this->session()->inst_set('loggedInAs', null);
/* Not logged in, you can't view, delete or edit the record */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
/* 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());
/* 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());
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
public function testAuthorisedMembersCanManipulateOthersRecords() {
$extensions = $this->removeExtensions(Object::get_extensions('Member'));
$member = $this->objFromFixture('Member', 'test');
$member2 = $this->objFromFixture('Member', 'staffmember');
/* 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());
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
public function testExtendedCan() {
$extensions = $this->removeExtensions(Object::get_extensions('Member'));
$member = $this->objFromFixture('Member', 'test');
/* 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());
/* 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');
$this->assertTrue($member2->canView());
$this->assertFalse($member2->canDelete());
$this->assertFalse($member2->canEdit());
/* Apply a extension that denies viewing of the Member */
Member::remove_extension('MemberTest_ViewingAllowedExtension');
Member::add_extension('MemberTest_ViewingDeniedExtension');
$member3 = $this->objFromFixture('Member', 'managementmember');
$this->assertFalse($member3->canView());
$this->assertFalse($member3->canDelete());
$this->assertFalse($member3->canEdit());
/* 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');
$this->assertTrue($member4->canView());
$this->assertFalse($member4->canDelete());
$this->assertTrue($member4->canEdit());
Member::remove_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
$this->addExtensions($extensions);
}
/**
* 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());
}
public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() {
$adminMember = $this->objFromFixture('Member', 'admin');
$otherAdminMember = $this->objFromFixture('Member', 'other-admin');
$securityAdminMember = $this->objFromFixture('Member', 'test');
$ceoMember = $this->objFromFixture('Member', 'ceomember');
// Careful: Don't read as english language.
// More precisely this should read canBeEditedBy()
$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');
$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');
}
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();
$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');
}
/**
* 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');
}
/**
* 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();
$admin = $this->objFromFixture('Member', 'admin');
$otherAdmin = $this->objFromFixture('Member', 'other-admin');
$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.
*
* @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.
*
* @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();
$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(
'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);
// A wrong token or a wrong device ID should not let us autologin
$response = $this->get(
'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(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => str_rot13($firstHash->DeviceID)
)
);
$this->assertNotContains($message, $response->getBody());
// 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(
'Security/LoginForm',
array(
'Email' => $m1->Email,
'Password' => '1nitialPassword',
'AuthenticationMethod' => 'MemberAuthenticator',
'action_dologin' => 'action_dologin'
),
null,
$this->session(),
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(
'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(
'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(
'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(
'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(
'Security/logout',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$secondToken,
'alc_device' => $secondHash->DeviceID
)
);
$this->assertEquals(
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(
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"
);
}
}
public function testCustomMemberValidator() {
$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 on 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');
}
}
/**
* @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'));
}
}