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 SilverStripe\ORM\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 SilverStripe\ORM\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('SilverStripe\\ORM\\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() { Config::inst()->update('Member', 'notify_password_change', true); $this->clearEmails(); $member = $this->objFromFixture('Member', 'test'); $this->assertNotNull($member); $valid = $member->changePassword('32asDF##$$%%'); $this->assertTrue($valid->valid()); $this->assertEmailSent('testuser@example.com', null, 'Your password has been changed', '/testuser@example\.com/'); } /** * Test that triggering "forgotPassword" sends an Email with a reset link */ public function testForgotPasswordEmaling() { $this->clearEmails(); $this->autoFollowRedirection = false; $member = $this->objFromFixture('Member', 'test'); $this->assertNotNull($member); // Initiate a password-reset $response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email)); $this->assertEquals($response->getStatusCode(), 302); // We should get redirected to Security/passwordsent $this->assertContains('Security/passwordsent/testuser@example.com', urldecode($response->getHeader('Location'))); // Check existance of reset link $this->assertEmailSent("testuser@example.com", null, 'Your password reset link', '/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/'); } /** * 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(); DBDateTime::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(); DBDateTime::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); DBDatetime::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 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')); } }