1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Unit tests for login lib. 19 * 20 * @package core 21 * @copyright 2017 Juan Leyva 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core; 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 global $CFG; 29 require_once($CFG->dirroot . '/login/lib.php'); 30 31 /** 32 * Login lib testcase. 33 * 34 * @package core 35 * @copyright 2017 Juan Leyva 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class login_lib_test extends \advanced_testcase { 39 40 public function test_core_login_process_password_reset_one_time_without_username_protection() { 41 global $CFG; 42 43 $this->resetAfterTest(); 44 $CFG->protectusernames = 0; 45 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual')); 46 47 $sink = $this->redirectEmails(); 48 49 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 50 $this->assertSame('emailresetconfirmsent', $status); 51 $emails = $sink->get_messages(); 52 $this->assertCount(1, $emails); 53 $email = reset($emails); 54 $this->assertSame($user->email, $email->to); 55 $this->assertNotEmpty($email->header); 56 $this->assertNotEmpty($email->body); 57 $this->assertMatchesRegularExpression('/A password reset was requested for your account/', 58 quoted_printable_decode($email->body)); 59 $sink->clear(); 60 } 61 62 public function test_core_login_process_password_reset_two_consecutive_times_without_username_protection() { 63 global $CFG; 64 65 $this->resetAfterTest(); 66 $CFG->protectusernames = 0; 67 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual')); 68 69 $sink = $this->redirectEmails(); 70 71 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 72 $this->assertSame('emailresetconfirmsent', $status); 73 // Request for a second time. 74 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 75 $this->assertSame('emailresetconfirmsent', $status); 76 $emails = $sink->get_messages(); 77 $this->assertCount(2, $emails); // Two emails sent (one per each request). 78 $email = array_pop($emails); 79 $this->assertSame($user->email, $email->to); 80 $this->assertNotEmpty($email->header); 81 $this->assertNotEmpty($email->body); 82 $this->assertMatchesRegularExpression('/A password reset was requested for your account/', 83 quoted_printable_decode($email->body)); 84 $sink->clear(); 85 } 86 87 public function test_core_login_process_password_reset_three_consecutive_times_without_username_protection() { 88 global $CFG; 89 90 $this->resetAfterTest(); 91 $CFG->protectusernames = 0; 92 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual')); 93 94 $sink = $this->redirectEmails(); 95 96 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 97 $this->assertSame('emailresetconfirmsent', $status); 98 // Request for a second time. 99 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 100 $this->assertSame('emailresetconfirmsent', $status); 101 // Third time. 102 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 103 $this->assertSame('emailalreadysent', $status); 104 $emails = $sink->get_messages(); 105 $this->assertCount(2, $emails); // Third time email is not sent. 106 } 107 108 public function test_core_login_process_password_reset_one_time_with_username_protection() { 109 global $CFG; 110 111 $this->resetAfterTest(); 112 $CFG->protectusernames = 1; 113 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual')); 114 115 $sink = $this->redirectEmails(); 116 117 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 118 $this->assertSame('emailpasswordconfirmmaybesent', $status); // Generic message not giving clues. 119 $emails = $sink->get_messages(); 120 $this->assertCount(1, $emails); 121 $email = reset($emails); 122 $this->assertSame($user->email, $email->to); 123 $this->assertNotEmpty($email->header); 124 $this->assertNotEmpty($email->body); 125 $this->assertMatchesRegularExpression('/A password reset was requested for your account/', 126 quoted_printable_decode($email->body)); 127 $sink->clear(); 128 } 129 130 public function test_core_login_process_password_reset_with_preexisting_expired_request_without_username_protection() { 131 global $CFG, $DB; 132 133 $this->resetAfterTest(); 134 $CFG->protectusernames = 0; 135 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual')); 136 137 $sink = $this->redirectEmails(); 138 139 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 140 $this->assertSame('emailresetconfirmsent', $status); 141 // Request again. 142 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 143 $this->assertSame('emailresetconfirmsent', $status); 144 145 $resetrequests = $DB->get_records('user_password_resets'); 146 $request = reset($resetrequests); 147 $request->timerequested = time() - YEARSECS; 148 $DB->update_record('user_password_resets', $request); 149 150 // Request again - third time - but it shuld be expired so we should get an email. 151 list($status, $notice, $url) = core_login_process_password_reset($user->username, null); 152 $this->assertSame('emailresetconfirmsent', $status); 153 $emails = $sink->get_messages(); 154 $this->assertCount(3, $emails); // Normal process, the previous request was deleted. 155 $email = reset($emails); 156 $this->assertSame($user->email, $email->to); 157 $this->assertNotEmpty($email->header); 158 $this->assertNotEmpty($email->body); 159 $this->assertMatchesRegularExpression('/A password reset was requested for your account/', 160 quoted_printable_decode($email->body)); 161 $sink->clear(); 162 } 163 164 public function test_core_login_process_password_reset_disabled_auth() { 165 $this->resetAfterTest(); 166 $user = $this->getDataGenerator()->create_user(array('auth' => 'oauth2')); 167 168 $sink = $this->redirectEmails(); 169 170 core_login_process_password_reset($user->username, null); 171 $emails = $sink->get_messages(); 172 $this->assertCount(1, $emails); 173 $email = reset($emails); 174 $this->assertSame($user->email, $email->to); 175 $this->assertNotEmpty($email->header); 176 $this->assertNotEmpty($email->body); 177 $this->assertMatchesRegularExpression('/Unfortunately your account on this site is disabled/', 178 quoted_printable_decode($email->body)); 179 $sink->clear(); 180 } 181 182 public function test_core_login_process_password_reset_auth_not_supporting_email_reset() { 183 global $CFG; 184 185 $this->resetAfterTest(); 186 $CFG->auth = $CFG->auth . ',mnet'; 187 $user = $this->getDataGenerator()->create_user(array('auth' => 'mnet')); 188 189 $sink = $this->redirectEmails(); 190 191 core_login_process_password_reset($user->username, null); 192 $emails = $sink->get_messages(); 193 $this->assertCount(1, $emails); 194 $email = reset($emails); 195 $this->assertSame($user->email, $email->to); 196 $this->assertNotEmpty($email->header); 197 $this->assertNotEmpty($email->body); 198 $this->assertMatchesRegularExpression('/Unfortunately passwords cannot be reset on this site/', 199 quoted_printable_decode($email->body)); 200 $sink->clear(); 201 } 202 203 public function test_core_login_process_password_reset_missing_parameters() { 204 $this->expectException('moodle_exception'); 205 $this->expectExceptionMessage(get_string('cannotmailconfirm', 'error')); 206 core_login_process_password_reset(null, null); 207 } 208 209 public function test_core_login_process_password_reset_invalid_username_with_username_protection() { 210 global $CFG; 211 $this->resetAfterTest(); 212 $CFG->protectusernames = 1; 213 list($status, $notice, $url) = core_login_process_password_reset('72347234nasdfasdf/Ds', null); 214 $this->assertEquals('emailpasswordconfirmmaybesent', $status); 215 } 216 217 public function test_core_login_process_password_reset_invalid_username_without_username_protection() { 218 global $CFG; 219 $this->resetAfterTest(); 220 $CFG->protectusernames = 0; 221 list($status, $notice, $url) = core_login_process_password_reset('72347234nasdfasdf/Ds', null); 222 $this->assertEquals('emailpasswordconfirmnotsent', $status); 223 } 224 225 public function test_core_login_process_password_reset_invalid_email_without_username_protection() { 226 global $CFG; 227 $this->resetAfterTest(); 228 $CFG->protectusernames = 0; 229 list($status, $notice, $url) = core_login_process_password_reset(null, 'fakeemail@nofd.zdy'); 230 $this->assertEquals('emailpasswordconfirmnotsent', $status); 231 } 232 233 /** 234 * Data provider for \core_login_lib_testcase::test_core_login_validate_forgot_password_data(). 235 */ 236 public function forgot_password_data_provider() { 237 return [ 238 'Both username and password supplied' => [ 239 [ 240 'username' => 's1', 241 'email' => 's1@example.com' 242 ], 243 [ 244 'username' => get_string('usernameoremail'), 245 'email' => get_string('usernameoremail'), 246 ] 247 ], 248 'Valid username' => [ 249 ['username' => 's1'] 250 ], 251 'Valid username, different case' => [ 252 ['username' => 'S1'] 253 ], 254 'Valid username, different case, username protection off' => [ 255 ['username' => 'S1'], 256 [], 257 ['protectusernames' => 0] 258 ], 259 'Non-existent username' => [ 260 ['username' => 's2'], 261 ], 262 'Non-existing username, username protection off' => [ 263 ['username' => 's2'], 264 ['username' => get_string('usernamenotfound')], 265 ['protectusernames' => 0] 266 ], 267 'Valid username, unconfirmed username, username protection on' => [ 268 ['username' => 's1'], 269 [], 270 ['confirmed' => 0] 271 ], 272 'Invalid email' => [ 273 ['email' => 's1-example.com'], 274 ['email' => get_string('invalidemail')] 275 ], 276 'Multiple accounts with the same email, username protection on' => [ 277 ['email' => 's1@example.com'], 278 [], 279 ['allowaccountssameemail' => 1] 280 ], 281 'Multiple accounts with the same email, username protection off' => [ 282 ['email' => 's1@example.com'], 283 ['email' => get_string('forgottenduplicate')], 284 ['allowaccountssameemail' => 1, 'protectusernames' => 0] 285 ], 286 'Multiple accounts with the same email but with different case, username protection is on' => [ 287 ['email' => 'S1@EXAMPLE.COM'], 288 [], 289 ['allowaccountssameemail' => 1] 290 ], 291 'Multiple accounts with the same email but with different case, username protection is off' => [ 292 ['email' => 'S1@EXAMPLE.COM'], 293 ['email' => get_string('forgottenduplicate')], 294 ['allowaccountssameemail' => 1, 'protectusernames' => 0] 295 ], 296 'Non-existent email, username protection on' => [ 297 ['email' => 's2@example.com'] 298 ], 299 'Non-existent email, username protection off' => [ 300 ['email' => 's2@example.com'], 301 ['email' => get_string('emailnotfound')], 302 ['protectusernames' => 0] 303 ], 304 'Valid email' => [ 305 ['email' => 's1@example.com'] 306 ], 307 'Valid email, different case' => [ 308 ['email' => 'S1@EXAMPLE.COM'] 309 ], 310 'Valid email, unconfirmed user, username protection is on' => [ 311 ['email' => 's1@example.com'], 312 [], 313 ['confirmed' => 0] 314 ], 315 'Valid email, unconfirmed user, username protection is off' => [ 316 ['email' => 's1@example.com'], 317 ['email' => get_string('confirmednot')], 318 ['confirmed' => 0, 'protectusernames' => 0] 319 ], 320 ]; 321 } 322 323 /** 324 * Test for core_login_validate_forgot_password_data(). 325 * 326 * @dataProvider forgot_password_data_provider 327 * @param array $data Key-value array containing username and email data. 328 * @param array $errors Key-value array containing error messages for the username and email fields. 329 * @param array $options Options for $CFG->protectusernames, $CFG->allowaccountssameemail and $user->confirmed. 330 */ 331 public function test_core_login_validate_forgot_password_data($data, $errors = [], $options = []) { 332 $this->resetAfterTest(); 333 334 // Set config settings we need for our environment. 335 $protectusernames = $options['protectusernames'] ?? 1; 336 set_config('protectusernames', $protectusernames); 337 338 $allowaccountssameemail = $options['allowaccountssameemail'] ?? 0; 339 set_config('allowaccountssameemail', $allowaccountssameemail); 340 341 // Generate the user data. 342 $generator = $this->getDataGenerator(); 343 $userdata = [ 344 'username' => 's1', 345 'email' => 's1@example.com', 346 'confirmed' => $options['confirmed'] ?? 1 347 ]; 348 $generator->create_user($userdata); 349 350 if ($allowaccountssameemail) { 351 // Create another user with the same email address. 352 $generator->create_user(['email' => 's1@example.com']); 353 } 354 355 // Validate the data. 356 $validationerrors = core_login_validate_forgot_password_data($data); 357 358 // Check validation errors for the username field. 359 if (isset($errors['username'])) { 360 // If we expect and error for the username field, confirm that it's set. 361 $this->assertArrayHasKey('username', $validationerrors); 362 // And the actual validation error is equal to the expected validation error. 363 $this->assertEquals($errors['username'], $validationerrors['username']); 364 } else { 365 // If we don't expect that there's a validation for the username field, confirm that it's not set. 366 $this->assertArrayNotHasKey('username', $validationerrors); 367 } 368 369 // Check validation errors for the email field. 370 if (isset($errors['email'])) { 371 // If we expect and error for the email field, confirm that it's set. 372 $this->assertArrayHasKey('email', $validationerrors); 373 // And the actual validation error is equal to the expected validation error. 374 $this->assertEquals($errors['email'], $validationerrors['email']); 375 } else { 376 // If we don't expect that there's a validation for the email field, confirm that it's not set. 377 $this->assertArrayNotHasKey('email', $validationerrors); 378 } 379 } 380 381 /** 382 * Test searching for the user record by matching the provided email address when resetting password. 383 * 384 * Email addresses should be handled as case-insensitive but accent sensitive. 385 */ 386 public function test_core_login_process_password_reset_email_sensitivity() { 387 global $CFG; 388 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); 389 390 $this->resetAfterTest(); 391 $sink = $this->redirectEmails(); 392 $CFG->protectusernames = 0; 393 394 // In this test, we need to mock sending emails on non-ASCII email addresses. However, such email addresses do 395 // not pass the default `validate_email()` and Moodle does not yet provide a CFG switch to allow such emails. 396 // So we inject our own validation method here and revert it back once we are done. This custom validator method 397 // is identical to the default 'php' validator with the only difference: it has the FILTER_FLAG_EMAIL_UNICODE 398 // set so that it allows to use non-ASCII characters in email addresses. 399 $defaultvalidator = \moodle_phpmailer::$validator; 400 \moodle_phpmailer::$validator = function($address) { 401 return (bool) filter_var($address, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE); 402 }; 403 404 // Emails are treated as case-insensitive when searching for the matching user account. 405 $u1 = $this->getDataGenerator()->create_user(['email' => 'priliszlutouckykunupeldabelskeody@example.com']); 406 407 list($status, $notice, $url) = core_login_process_password_reset(null, 'PrIlIsZlUtOuCkYKuNupELdAbElSkEoDy@eXaMpLe.CoM'); 408 409 $this->assertSame('emailresetconfirmsent', $status); 410 $emails = $sink->get_messages(); 411 $this->assertCount(1, $emails); 412 $email = reset($emails); 413 $this->assertSame($u1->email, $email->to); 414 $sink->clear(); 415 416 // There may exist two users with same emails. 417 $u2 = $this->getDataGenerator()->create_user(['email' => 'PRILISZLUTOUCKYKUNUPELDABELSKEODY@example.com']); 418 419 list($status, $notice, $url) = core_login_process_password_reset(null, 'PrIlIsZlUtOuCkYKuNupELdAbElSkEoDy@eXaMpLe.CoM'); 420 421 $this->assertSame('emailresetconfirmsent', $status); 422 $emails = $sink->get_messages(); 423 $this->assertCount(1, $emails); 424 $email = reset($emails); 425 $this->assertSame(\core_text::strtolower($u2->email), \core_text::strtolower($email->to)); 426 $sink->clear(); 427 428 // However, emails are accent sensitive - note this is the u1's email with a single character a -> á changed. 429 list($status, $notice, $url) = core_login_process_password_reset(null, 'priliszlutouckykunupeldábelskeody@example.com'); 430 431 $this->assertSame('emailpasswordconfirmnotsent', $status); 432 $emails = $sink->get_messages(); 433 $this->assertCount(0, $emails); 434 $sink->clear(); 435 436 $u3 = $this->getDataGenerator()->create_user(['email' => 'PřílišŽluťoučkýKůňÚpělĎálebskéÓdy@example.com']); 437 438 list($status, $notice, $url) = core_login_process_password_reset(null, 'pŘÍLIŠžLuŤOuČkÝkŮŇúPĚLďÁLEBSKÉóDY@eXaMpLe.CoM'); 439 440 $this->assertSame('emailresetconfirmsent', $status); 441 $emails = $sink->get_messages(); 442 $this->assertCount(1, $emails); 443 $email = reset($emails); 444 $this->assertSame($u3->email, $email->to); 445 $sink->clear(); 446 447 // Restore the original email address validator. 448 \moodle_phpmailer::$validator = $defaultvalidator; 449 } 450 451 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body