You are here

password_policy_expiration.test in Password Policy 7

Tests for Password policy module expiration functionality.

File

tests/password_policy_expiration.test
View source
<?php

/**
 * @file
 * Tests for Password policy module expiration functionality.
 */

/**
 * Tests of password expiration.
 */
class PasswordPolicyExpirationTestCase extends DrupalWebTestCase {
  protected $admin;
  protected $policyMaker;

  /**
   * Gets info about the test case.
   */
  public static function getInfo() {
    return array(
      'name' => 'Password expiration',
      'description' => 'Test password expiration and related settings.',
      'group' => 'Password Policy',
    );
  }

  /**
   * Sets up the test.
   */
  public function setUp() {
    parent::setUp('password_policy');
    $this
      ->createAdmin();
    $this
      ->createPolicyMaker();
  }

  /**
   * Creates a usable admin (UID=1) user.
   *
   * SimpleTest creates an admin user, but it cannot log in since it has no
   * password set. So, we give it a password.
   */
  protected function createAdmin() {
    global $user;
    $pass = user_password();
    $edit = array(
      'pass' => $pass,
    );
    user_save($user, $edit);
    $user->pass_raw = $pass;
    $this->admin = $user;
  }

  /**
   * Creates policy maker.
   */
  protected function createPolicyMaker() {
    $this->policyMaker = $this
      ->drupalCreateUser(array(
      'administer users',
      'administer permissions',
      'administer password policies',
      'unblock expired accounts',
    ));
  }

  /**
   * Tests expiration constraint.
   */
  public function testExpirationConstraint() {

    // Create role to which the expiration policy will apply.
    // It is identical to the 'authenticated user' role in permissions. We
    // create this separate role because we would like the policy maker user to
    // be exempt from the expiration policy.
    $expiration_rid = $this
      ->drupalCreateRole(array());

    // Log in policy maker.
    $policy_maker_user = $this->policyMaker;
    $this
      ->drupalLogin($policy_maker_user);

    // Create a policy.
    $policy_name = $this
      ->createExpirationPolicy(1, $expiration_rid);

    // Verify that an expiration rule has been set in the policy.
    $pid = db_query('SELECT pid FROM {password_policy} WHERE name = :name', array(
      ':name' => $policy_name,
    ))
      ->fetchField();
    $expiration = db_query('SELECT expiration FROM {password_policy} WHERE pid = :pid', array(
      ':pid' => $pid,
    ))
      ->fetchField();
    $this
      ->verbose('Expiration: ' . var_export($expiration, TRUE));
    $this
      ->assertTrue($expiration == 1, 'Verified expiration set.');

    // Enable the policy.
    $this
      ->enablePolicy($policy_name);
    _password_policy_advance_test_clock(60 * 60 * 24 + 1);

    // Create an account to test with.
    $name1 = $this
      ->randomName();
    $pass1 = 'aaaaaa';
    $edit = array(
      'name' => $name1,
      'mail' => $name1 . '@example.com',
      'pass[pass1]' => $pass1,
      'pass[pass2]' => $pass1,
    );
    $this
      ->drupalPost('admin/people/create', $edit, 'Create new account');
    $uid = db_query('SELECT uid FROM {users} WHERE name = :name', array(
      ':name' => $name1,
    ))
      ->fetchField();
    $this
      ->drupalGet('user/' . $uid . '/edit');
    $this
      ->assertFieldChecked('edit-status-1', 'Account status is set to active.');

    // Add user to role covered by expiration policy.
    user_multiple_role_edit(array(
      $uid,
    ), 'add_role', $expiration_rid);

    // Log out and attempt to log in with the newly created test account.
    $this
      ->drupalLogout();
    $edit = array(
      'name' => $name1,
      'pass' => $pass1,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $name1,
    )), 'Account not blocked from logging in.');
    _password_policy_advance_test_clock(60 * 60 * 24 + 1);

    // Check that password should be expired, once cron runs.
    $created = db_query('SELECT created FROM {password_policy_history} WHERE uid = :uid', array(
      ':uid' => '3',
    ))
      ->fetchField();
    $this
      ->verbose('Created: ' . var_export($created, TRUE));
    $created = db_query('SELECT created FROM {password_policy} WHERE pid = :pid', array(
      ':pid' => $pid,
    ))
      ->fetchField();
    $this
      ->verbose('$pid ' . $pid . ' created: ' . var_export($created, TRUE));

    // Run cron to trigger password expirations.
    $this
      ->cronRun();

    // Check that test account has been blocked.
    $this
      ->drupalLogin($policy_maker_user);
    $this
      ->drupalGet('user/' . $uid . '/edit');
    $this
      ->assertFieldChecked('edit-status-0', 'Account status is set to blocked.');
    $this
      ->drupalGet('admin/people/expired');
    $this
      ->assertText('unblock', 'Account marked as blocked on Expired Accounts tab.');

    // Log out and attempt to log in to the expired account again, to verify
    // block.
    $this
      ->drupalLogout();
    $edit = array(
      'name' => $name1,
      'pass' => $pass1,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $name1,
    )), 'Account blocked from logging in.');

    // Log in as policy making user to unblock the test user.
    $this
      ->drupalLogin($policy_maker_user);
    $this
      ->drupalPost('admin/people/expired/unblock/' . $uid, array(), t('Unblock user'));
    $this
      ->assertText(t('The user !name has been unblocked.', array(
      '!name' => $name1,
    )), 'Account account has been unblocked.');
    $this
      ->drupalGet('admin/people/expired');
    $this
      ->assertNoText('unblock', 'Account not marked as blocked on Expired Accounts tab.');

    // Log out and attempt to log in the expired account again.
    $this
      ->drupalLogout();
    $edit = array(
      'name' => $name1,
      'pass' => $pass1,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $name1,
    )), 'Account not blocked from logging in.');
    $this
      ->assertNoText(t('User login'), 'Check that login block is not shown, to verify user successfully logged in.');
    $this
      ->assertRaw(t('Your password has expired. You must change your password to proceed on the site.'), 'User forced to change password.');

    // Change test user account's password.
    $pass2 = 'bbbbbb';
    $edit = array(
      'current_pass' => $pass1,
      'pass[pass1]' => $pass2,
      'pass[pass2]' => $pass2,
    );
    $this
      ->drupalPost('user/' . $uid . '/edit', $edit, t('Save'));
    $this
      ->assertText(t('The changes have been saved.'), format_string('1st password change: !pass', array(
      '!pass' => $pass2,
    )));
    $this
      ->drupalGet('node');
    $this
      ->drupalLogout();

    // Run cron to trigger password expirations.
    $this
      ->cronRun();

    // Log in to confirm password not seen as expired now that it has changed.
    $edit = array(
      'name' => $name1,
      'pass' => $pass1,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $name1,
    )), 'Account not blocked from logging in.');
    $this
      ->assertNoText(t('User login'), 'Check that login block is not shown, to verify user successfully logged in.');
    $this
      ->drupalLogout();

    // Delete test policy.
    $this
      ->drupalLogin($policy_maker_user);
    $this
      ->drupalPost('admin/config/people/password_policy/' . $pid . '/delete', array(), t('Delete'));
    $this
      ->assertText('Password policy ' . $policy_name . ' was deleted.', 'Default password policy ' . $policy_name . 'was deleted');
  }

  /**
   * Tests unblocking an expired account via user edit form.
   */
  public function testUnblockingExpiredAccountViaUserEditForm() {

    // Create an account to test with.
    $account = $this
      ->drupalCreateUser();
    $expiration_rid = $this
      ->assignToNewRole($account);

    // Set 180-day expiration policy.
    $expiration_days = 180;
    $this
      ->setExpirationPolicy($expiration_days, $expiration_rid);

    // Advance to at least one second past expiration.
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock($expiration_days * $one_day + 1);

    // Run cron to trigger password expirations.
    $this
      ->cronRun();

    // Confirm account blocked.
    $edit = array(
      'name' => $account->name,
      'pass' => $account->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $account->name,
    )), 'Account blocked from logging in.');

    // Unblock account using user edit form.
    $admin = $this->admin;
    $this
      ->drupalLogin($admin);
    $edit = array(
      'status' => 1,
    );
    $this
      ->drupalPost("user/{$account->uid}/edit", $edit, t('Save'));
    $this
      ->drupalLogout();

    // Run cron since it could errantly reblock account.
    $this
      ->cronRun();

    // Confirm account unblocked.
    $edit = array(
      'name' => $account->name,
      'pass' => $account->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $account->name,
    )), 'Account not blocked from logging in.');
  }

  /**
   * Tests unblocking a "reblocked" account.
   *
   * By "reblocked" account we mean one that was blocked due to password
   * expiration, unblocked, then blocked again due to the password not being
   * changed within 24 hours.
   */
  public function testUnblockingReblockedAccount() {

    // Create role to which the expiration policy will apply.
    $expiration_rid = $this
      ->drupalCreateRole(array());

    // Add user covered by expiration policy.
    $account = $this
      ->drupalCreateUser();
    user_multiple_role_edit(array(
      $account->uid,
    ), 'add_role', $expiration_rid);

    // Set an expiration policy.
    $expiration_days = 7;
    $this
      ->setExpirationPolicy($expiration_days, $expiration_rid);

    // Allow the password to expire.
    $seven_days_one_second = 60 * 60 * 24 * 7 + 1;
    _password_policy_advance_test_clock($seven_days_one_second);
    $this
      ->cronRun();

    // Log in as policy making user to unblock the test user.
    $this
      ->drupalLogin($this->policyMaker);
    $this
      ->drupalPost('admin/people/expired/unblock/' . $account->uid, array(), t('Unblock user'));
    $this
      ->assertText(t('The user !name has been unblocked.', array(
      '!name' => $account->name,
    )), 'Account account has been unblocked.');
    $this
      ->drupalGet('admin/people/expired');
    $this
      ->assertNoText('unblock', 'Account not marked as blocked on Expired Accounts tab.');
    $this
      ->drupalLogout();

    // Wait over one day so account is reblocked.
    $one_day_one_second = 60 * 60 * 24 + 1;
    _password_policy_advance_test_clock($one_day_one_second);
    $this
      ->cronRun();

    // Attempt to log in. User should be blocked.
    $edit = array(
      'name' => $account->name,
      'pass' => $account->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $account->name,
    )), 'Account blocked from logging in.');

    // Log in as policy making user to unblock the test user.
    $this
      ->drupalLogin($this->policyMaker);
    $this
      ->drupalPost('admin/people/expired/unblock/' . $account->uid, array(), t('Unblock user'));
    $this
      ->assertText(t('The user !name has been unblocked.', array(
      '!name' => $account->name,
    )), 'Account account has been unblocked.');
    $this
      ->drupalGet('admin/people/expired');
    $this
      ->assertNoText('unblock', 'Account not marked as blocked on Expired Accounts tab.');
    $this
      ->drupalLogout();

    // Attempt to log in. User should not be blocked.
    $edit = array(
      'name' => $account->name,
      'pass' => $account->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $account->name,
    )), 'Account not blocked from logging in.');
    $this
      ->drupalLogout();
  }

  /**
   * Tests "Admin (UID=1) password expires" being disabled (i.e., unchecked).
   */
  public function testAdminExpirationDisabled() {
    $this
      ->disableAdminExpiration();

    // Set an expiration policy.
    $expiration_days = 30;
    $this
      ->setExpirationPolicy($expiration_days);
    $this
      ->cronRun();

    // Advance to at least one second past expiration.
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock($expiration_days * $one_day + 1);
    $this
      ->cronRun();

    // Attempt login as admin and confirm password not expired.
    $admin = $this->admin;
    $edit = array(
      'name' => $admin->name,
      'pass' => $admin->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertNoText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $admin->name,
    )), 'Admin not blocked from logging in.');
  }

  /**
   * Tests "Admin (UID=1) password expires" being enabled (i.e., checked).
   *
   * This is the default setting.
   */
  public function testAdminExpirationEnabled() {

    // Set an expiration policy.
    $expiration_days = 30;
    $this
      ->setExpirationPolicy($expiration_days);
    $this
      ->cronRun();

    // Advance to at least one second past expiration.
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock($expiration_days * $one_day + 1);
    $this
      ->cronRun();

    // Attempt login as admin and confirm password expired.
    $admin = $this->admin;
    $edit = array(
      'name' => $admin->name,
      'pass' => $admin->pass_raw,
    );
    $this
      ->drupalPost('user/login', $edit, t('Log in'));
    $this
      ->assertText(t('The username !name has not been activated or is blocked.', array(
      '!name' => $admin->name,
    )), 'Admin blocked from logging in.');
  }

  /**
   * Tests expiration warning e-mails.
   */
  public function testWarningEmails() {
    $expiration_days = 180;
    $rid = DRUPAL_AUTHENTICATED_RID;
    $warning = '14,7';
    $this
      ->setExpirationPolicy($expiration_days, $rid, $warning);

    // Disable admin (UID=1) expiration to prevent multiple expiration emails.
    $this
      ->disableAdminExpiration();

    // Start at 180 days before expiration.
    // By "day" in the rest of this method, a 24-hour period of time is meant,
    // not a calendar day.
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent initially.');

    // Advance to one second past 90 days before expiration.
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock(90 * $one_day + 1);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent before first expiration warning day.');

    // Advance to one second past 14 days before expiration.
    _password_policy_advance_test_clock(76 * $one_day);
    $this
      ->cronRun();
    $warning_subject = t('Password expiration warning for !username at !site', array(
      '!username' => $this->policyMaker->name,
      '!site' => variable_get('site_name', 'Drupal'),
    ));
    $this
      ->assertMail('subject', $warning_subject, 'First expiration warning e-mail sent.');
    $this
      ->clearMails();

    // Advance to one second past 10 days before expiration.
    _password_policy_advance_test_clock(4 * $one_day);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent between first and second expiration warning days.');

    // Advance to one second past 7 days before expiration.
    _password_policy_advance_test_clock(3 * $one_day);
    $this
      ->cronRun();
    $warning_subject = t('Password expiration warning for !username at !site', array(
      '!username' => $this->policyMaker->name,
      '!site' => variable_get('site_name', 'Drupal'),
    ));
    $this
      ->assertMail('subject', $warning_subject, 'Second expiration warning e-mail sent.');
    $this
      ->clearMails();

    // Advance to one second past 6 days before expiration.
    _password_policy_advance_test_clock(1 * $one_day);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent after expiration warnings and before expiration.');

    // Advance to one second past day before expiration.
    _password_policy_advance_test_clock(6 * $one_day);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent after expiration warnings and less than one day before expiration.');

    // Advance to one second past 3 days after expiration.
    _password_policy_advance_test_clock(3 * $one_day);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent after expiration.');
  }

  /**
   * Tests warning e-mails not sent when they are disabled.
   *
   * Limitation: This method does not test that e-mails are not sent on the
   * exact second of expiration.
   */
  public function testWarningEmailsDisabled() {
    $expiration_days = 2;
    $rid = DRUPAL_AUTHENTICATED_RID;
    $warning = '';
    $this
      ->setExpirationPolicy($expiration_days, $rid, $warning);

    // Run cron at least one second past two days before expiration.
    // By "day" in the rest of this method, a 24-hour period of time is meant,
    // not a calendar day.
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock($one_day + 1);
    $this
      ->cronRun();

    // Run cron at least one second past day before expiration.
    _password_policy_advance_test_clock($one_day);
    $this
      ->cronRun();

    // Run cron at least one second past expiration.
    _password_policy_advance_test_clock($one_day);
    $this
      ->cronRun();
    $this
      ->assertNoMail('No e-mails sent.');
  }

  /**
   * Tests tokens replacement in warning e-mails.
   *
   * Importantly, this tests the Password Policy tokens.
   */
  public function testWarningEmailsTokens() {
    $expiration_days = 30;
    $rid = DRUPAL_AUTHENTICATED_RID;
    $warning = '14';
    $this
      ->setExpirationPolicy($expiration_days, $rid, $warning);

    // Disable admin (UID=1) expiration to prevent multiple expiration emails.
    $this
      ->disableAdminExpiration();

    // Trigger expiration warning e-mail.
    // (Advance to one second past 14 days before expiration and run cron.)
    $one_day = 24 * 60 * 60;
    _password_policy_advance_test_clock(16 * $one_day + 1);
    $this
      ->cronRun();

    // Assert correct subject.
    $warning_subject = t('Password expiration warning for !username at !site', array(
      '!username' => $this->policyMaker->name,
      '!site' => variable_get('site_name', 'Drupal'),
    ));
    $this
      ->assertMail('subject', $warning_subject, "Tokens replaced in subject correctly.");

    // Assert correct body.
    $edit_url = _password_policy_get_preferred_password_edit_url_for_user($this->policyMaker);
    global $language;
    $langcode = isset($language) ? $language->language : NULL;
    $warning_body = t("!username,\n\nYour password at !site will expire in less than !days_left day(s).\n\nPlease go to !edit_url to change your password.", array(
      '!username' => $this->policyMaker->name,
      '!site' => variable_get('site_name', 'Drupal'),
      '!days_left' => '14',
      '!edit_url' => $edit_url,
    ), array(
      'langcode' => $langcode,
    ));

    // assertMail() does not seem to work for the body because the mail
    // functions insert some whitespace into the body. assertMailString() seems
    // to work, though.
    $this
      ->assertMailString('body', $warning_body, 1);
  }

  /**
   * Sets expiration policy.
   *
   * Has Policy Maker user create and enable an expiration policy.
   *
   * @param int $expiration
   *   Number of days after which password expires.
   * @param int $rid
   *   (optional) Role ID to which this policy should apply.
   * @param string $warning
   *   (optional) Comma-delimited list of numbers of days before expiration on
   *   which expiration warnings are to be sent.
   */
  protected function setExpirationPolicy($expiration, $rid = DRUPAL_AUTHENTICATED_RID, $warning = '') {
    $policy_maker = $this->policyMaker;
    $this
      ->drupalLogin($policy_maker);
    $name = $this
      ->createExpirationPolicy($expiration, $rid, $warning);
    $this
      ->enablePolicy($name);
    $this
      ->drupalLogout();
  }

  /**
   * Disables "Admin (UID=1) password expires" setting.
   */
  protected function disableAdminExpiration() {
    $admin = $this->admin;
    $this
      ->drupalLogin($admin);
    $edit = array(
      'password_policy_admin' => FALSE,
    );
    $this
      ->drupalPost('admin/config/people/password_policy', $edit, t('Save configuration'));
    $this
      ->drupalLogout();
  }

  /**
   * Creates expiration policy.
   *
   * @param int $expiration
   *   Number of days after which password expires.
   * @param int $rid
   *   (optional) Role ID to which this policy should apply.
   * @param string $warning
   *   (optional) Comma-delimited list of numbers of days before expiration on
   *   which expiration warnings are to be sent.
   *
   * @return string
   *   Name of created policy.
   */
  protected function createExpirationPolicy($expiration, $rid = DRUPAL_AUTHENTICATED_RID, $warning = '') {
    $policy_name = $this
      ->randomName();
    $edit = array(
      'name' => $policy_name,
      'expiration' => $expiration,
      'warning' => $warning,
      "roles[{$rid}]" => $rid,
    );
    $this
      ->drupalPost('admin/config/people/password_policy/add', $edit, t('Create'));
    $created_text = "Policy {$policy_name} has been created.";
    $this
      ->assertText($created_text, $created_text);
    return $policy_name;
  }

  /**
   * Enables policy.
   */
  protected function enablePolicy($policy_name) {
    $pid = db_query('SELECT pid FROM {password_policy} WHERE name = :name', array(
      ':name' => $policy_name,
    ))
      ->fetchField();
    $edit = array(
      "policies[{$pid}][enabled]" => $pid,
    );
    $this
      ->drupalPost('admin/config/people/password_policy/list', $edit, t('Save changes'));
    $this
      ->assertText(t('The changes have been saved.'), 'Form submitted successfully.');
    $this
      ->drupalGet('admin/config/people/password_policy');
    $enabled = db_query('SELECT enabled FROM {password_policy} WHERE pid = :pid', array(
      ':pid' => $pid,
    ))
      ->fetchField();
    $this
      ->assertTrue($enabled == 1, 'Policy enabled.');
  }

  /**
   * Assigns user to a newly created role.
   *
   * @param object $account
   *   User object.
   *
   * @return int
   *   Role ID of newly created role.
   */
  protected function assignToNewRole($account) {
    $expiration_rid = $this
      ->drupalCreateRole(array());
    user_multiple_role_edit(array(
      $account->uid,
    ), 'add_role', $expiration_rid);
    return $expiration_rid;
  }

  /**
   * Asserts that no e-mail has been sent.
   *
   * Specifically, asserts that no e-mail has been sent since the e-mails
   * collected during the test were last cleared.
   */
  protected function assertNoMail($message) {
    $captured_emails = variable_get('drupal_test_email_collector', array());
    $this
      ->assertTrue(empty($captured_emails), $message);
  }

  /**
   * Clears all e-mails collected during test.
   */
  protected function clearMails() {
    variable_set('drupal_test_email_collector', array());
  }

}

Classes

Namesort descending Description
PasswordPolicyExpirationTestCase Tests of password expiration.