See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 namespace core; 18 19 use core\oauth2\access_token; 20 use core\oauth2\api; 21 use core\oauth2\endpoint; 22 use core\oauth2\issuer; 23 use core\oauth2\system_account; 24 use \core\oauth2\user_field_mapping; 25 26 /** 27 * Tests for oauth2 apis (\core\oauth2\*). 28 * 29 * @package core 30 * @copyright 2017 Damyon Wiese 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. 32 * @coversDefaultClass \core\oauth2\api 33 */ 34 class oauth2_test extends \advanced_testcase { 35 36 /** 37 * Tests the crud operations on oauth2 issuers. 38 */ 39 public function test_create_and_delete_standard_issuers() { 40 $this->resetAfterTest(); 41 $this->setAdminUser(); 42 api::create_standard_issuer('google'); 43 api::create_standard_issuer('facebook'); 44 api::create_standard_issuer('microsoft'); 45 api::create_standard_issuer('nextcloud', 'https://dummy.local/nextcloud/'); 46 47 $issuers = api::get_all_issuers(); 48 49 $this->assertEquals($issuers[0]->get('name'), 'Google'); 50 $this->assertEquals($issuers[1]->get('name'), 'Facebook'); 51 $this->assertEquals($issuers[2]->get('name'), 'Microsoft'); 52 $this->assertEquals($issuers[3]->get('name'), 'Nextcloud'); 53 54 api::move_down_issuer($issuers[0]->get('id')); 55 56 $issuers = api::get_all_issuers(); 57 58 $this->assertEquals($issuers[0]->get('name'), 'Facebook'); 59 $this->assertEquals($issuers[1]->get('name'), 'Google'); 60 $this->assertEquals($issuers[2]->get('name'), 'Microsoft'); 61 $this->assertEquals($issuers[3]->get('name'), 'Nextcloud'); 62 63 api::delete_issuer($issuers[1]->get('id')); 64 65 $issuers = api::get_all_issuers(); 66 67 $this->assertEquals($issuers[0]->get('name'), 'Facebook'); 68 $this->assertEquals($issuers[1]->get('name'), 'Microsoft'); 69 $this->assertEquals($issuers[2]->get('name'), 'Nextcloud'); 70 } 71 72 /** 73 * Tests the crud operations on oauth2 issuers. 74 */ 75 public function test_create_nextcloud_without_url() { 76 $this->resetAfterTest(); 77 $this->setAdminUser(); 78 79 $this->expectException(\moodle_exception::class); 80 api::create_standard_issuer('nextcloud'); 81 } 82 83 /** 84 * Tests we can list and delete each of the persistents related to an issuer. 85 */ 86 public function test_getters() { 87 $this->resetAfterTest(); 88 $this->setAdminUser(); 89 $issuer = api::create_standard_issuer('microsoft'); 90 91 $same = api::get_issuer($issuer->get('id')); 92 93 foreach ($same->properties_definition() as $name => $def) { 94 $this->assertTrue($issuer->get($name) == $same->get($name)); 95 } 96 97 $endpoints = api::get_endpoints($issuer); 98 $same = api::get_endpoint($endpoints[0]->get('id')); 99 $this->assertEquals($endpoints[0]->get('id'), $same->get('id')); 100 $this->assertEquals($endpoints[0]->get('name'), $same->get('name')); 101 102 $todelete = $endpoints[0]; 103 api::delete_endpoint($todelete->get('id')); 104 $endpoints = api::get_endpoints($issuer); 105 $this->assertNotEquals($endpoints[0]->get('id'), $todelete->get('id')); 106 107 $userfields = api::get_user_field_mappings($issuer); 108 $same = api::get_user_field_mapping($userfields[0]->get('id')); 109 $this->assertEquals($userfields[0]->get('id'), $same->get('id')); 110 111 $todelete = $userfields[0]; 112 api::delete_user_field_mapping($todelete->get('id')); 113 $userfields = api::get_user_field_mappings($issuer); 114 $this->assertNotEquals($userfields[0]->get('id'), $todelete->get('id')); 115 } 116 117 /** 118 * Data provider for \core_oauth2_testcase::test_get_system_oauth_client(). 119 * 120 * @return array 121 */ 122 public function system_oauth_client_provider() { 123 return [ 124 [ 125 (object) [ 126 'access_token' => 'fdas...', 127 'token_type' => 'Bearer', 128 'expires_in' => '3600', 129 'id_token' => 'llfsd..', 130 ], HOURSECS - 10 131 ], 132 [ 133 (object) [ 134 'access_token' => 'fdas...', 135 'token_type' => 'Bearer', 136 'id_token' => 'llfsd..', 137 ], WEEKSECS 138 ], 139 ]; 140 } 141 142 /** 143 * Tests we can get a logged in oauth client for a system account. 144 * 145 * @dataProvider system_oauth_client_provider 146 * @param \stdClass $responsedata The response data to be mocked. 147 * @param int $expiresin The expected expiration time. 148 */ 149 public function test_get_system_oauth_client($responsedata, $expiresin) { 150 $this->resetAfterTest(); 151 $this->setAdminUser(); 152 153 $issuer = api::create_standard_issuer('microsoft'); 154 155 $requiredscopes = api::get_system_scopes_for_issuer($issuer); 156 // Fake a system account. 157 $data = (object) [ 158 'issuerid' => $issuer->get('id'), 159 'refreshtoken' => 'abc', 160 'grantedscopes' => $requiredscopes, 161 'email' => 'sys@example.com', 162 'username' => 'sys' 163 ]; 164 $sys = new system_account(0, $data); 165 $sys->create(); 166 167 // Fake a response with an access token. 168 $response = json_encode($responsedata); 169 \curl::mock_response($response); 170 $client = api::get_system_oauth_client($issuer); 171 $this->assertTrue($client->is_logged_in()); 172 173 // Check token expiry. 174 $accesstoken = access_token::get_record(['issuerid' => $issuer->get('id')]); 175 176 // Get the difference between the actual and expected expiry times. 177 // They might differ by a couple of seconds depending on the timing when the token gets actually processed. 178 $expiresdifference = time() + $expiresin - $accesstoken->get('expires'); 179 180 // Assert that the actual token expiration is more or less the same as the expected. 181 $this->assertGreaterThanOrEqual(0, $expiresdifference); 182 $this->assertLessThanOrEqual(3, $expiresdifference); 183 } 184 185 /** 186 * Tests we can enable and disable an issuer. 187 */ 188 public function test_enable_disable_issuer() { 189 $this->resetAfterTest(); 190 $this->setAdminUser(); 191 192 $issuer = api::create_standard_issuer('microsoft'); 193 194 $issuerid = $issuer->get('id'); 195 196 api::enable_issuer($issuerid); 197 $check = api::get_issuer($issuer->get('id')); 198 $this->assertTrue((boolean)$check->get('enabled')); 199 200 api::enable_issuer($issuerid); 201 $check = api::get_issuer($issuer->get('id')); 202 $this->assertTrue((boolean)$check->get('enabled')); 203 204 api::disable_issuer($issuerid); 205 $check = api::get_issuer($issuer->get('id')); 206 $this->assertFalse((boolean)$check->get('enabled')); 207 208 api::enable_issuer($issuerid); 209 $check = api::get_issuer($issuer->get('id')); 210 $this->assertTrue((boolean)$check->get('enabled')); 211 } 212 213 /** 214 * Test the alloweddomains for an issuer. 215 */ 216 public function test_issuer_alloweddomains() { 217 $this->resetAfterTest(); 218 $this->setAdminUser(); 219 220 $issuer = api::create_standard_issuer('microsoft'); 221 222 $issuer->set('alloweddomains', ''); 223 224 // Anything is allowed when domain is empty. 225 $this->assertTrue($issuer->is_valid_login_domain('')); 226 $this->assertTrue($issuer->is_valid_login_domain('a@b')); 227 $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); 228 229 $issuer->set('alloweddomains', 'example.com'); 230 231 // One domain - must match exactly - no substrings etc. 232 $this->assertFalse($issuer->is_valid_login_domain('')); 233 $this->assertFalse($issuer->is_valid_login_domain('a@b')); 234 $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); 235 $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); 236 237 $issuer->set('alloweddomains', 'example.com,example.net'); 238 // Multiple domains - must match any exactly - no substrings etc. 239 $this->assertFalse($issuer->is_valid_login_domain('')); 240 $this->assertFalse($issuer->is_valid_login_domain('a@b')); 241 $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); 242 $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net')); 243 $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net')); 244 $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com')); 245 246 $issuer->set('alloweddomains', '*.example.com'); 247 // Wildcard. 248 $this->assertFalse($issuer->is_valid_login_domain('')); 249 $this->assertFalse($issuer->is_valid_login_domain('a@b')); 250 $this->assertFalse($issuer->is_valid_login_domain('longer.example@example')); 251 $this->assertFalse($issuer->is_valid_login_domain('longer.example@example.com')); 252 $this->assertTrue($issuer->is_valid_login_domain('longer.example@sub.example.com')); 253 } 254 255 /** 256 * Test endpoints creation for issuers. 257 * @dataProvider create_endpoints_for_standard_issuer_provider 258 * 259 * @covers ::create_endpoints_for_standard_issuer 260 * 261 * @param string $type Issuer type to create. 262 * @param string|null $discoveryurl Expected discovery URL or null if this endpoint doesn't exist. 263 * @param bool $hasmappingfields True if it's expected the issuer to create has mapping fields. 264 * @param string|null $baseurl The service URL (mandatory parameter for some issuers, such as NextCloud or IMS OBv2.1). 265 * @param string|null $expectedexception Name of the expected expection or null if no exception will be thrown. 266 */ 267 public function test_create_endpoints_for_standard_issuer(string $type, ?string $discoveryurl = null, 268 bool $hasmappingfields = true, ?string $baseurl = null, ?string $expectedexception = null): void { 269 270 $this->resetAfterTest(); 271 272 // Mark test as long because it connects with external services. 273 if (!PHPUNIT_LONGTEST) { 274 $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); 275 } 276 277 $this->setAdminUser(); 278 279 // Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer. 280 if ($expectedexception) { 281 $this->expectException($expectedexception); 282 } 283 $issuer = api::create_standard_issuer($type, $baseurl); 284 285 // Check endpoints have been created. 286 $endpoints = api::get_endpoints($issuer); 287 $this->assertNotEmpty($endpoints); 288 $this->assertNotEmpty($issuer->get('image')); 289 // Check discovery URL. 290 if ($discoveryurl) { 291 $this->assertStringContainsString($discoveryurl, $issuer->get_endpoint_url('discovery')); 292 } else { 293 $this->assertFalse($issuer->get_endpoint_url('discovery')); 294 } 295 // Check userfield mappings. 296 $userfieldmappings =api::get_user_field_mappings($issuer); 297 if ($hasmappingfields) { 298 $this->assertNotEmpty($userfieldmappings); 299 } else { 300 $this->assertEmpty($userfieldmappings); 301 } 302 } 303 304 /** 305 * Data provider for test_create_endpoints_for_standard_issuer. 306 * 307 * @return array 308 */ 309 public function create_endpoints_for_standard_issuer_provider(): array { 310 return [ 311 'Google' => [ 312 'type' => 'google', 313 'discoveryurl' => '.well-known/openid-configuration', 314 ], 315 'Google will work too with a valid baseurl parameter' => [ 316 'type' => 'google', 317 'discoveryurl' => '.well-known/openid-configuration', 318 'hasmappingfields' => true, 319 'baseurl' => 'https://accounts.google.com/', 320 ], 321 'IMS OBv2.1' => [ 322 'type' => 'imsobv2p1', 323 'discoveryurl' => '.well-known/badgeconnect.json', 324 'hasmappingfields' => false, 325 'baseurl' => 'https://dc.imsglobal.org/', 326 ], 327 'IMS OBv2.1 without slash in baseurl should work too' => [ 328 'type' => 'imsobv2p1', 329 'discoveryurl' => '.well-known/badgeconnect.json', 330 'hasmappingfields' => false, 331 'baseurl' => 'https://dc.imsglobal.org', 332 ], 333 'IMS OBv2.1 with empty baseurl should return an exception' => [ 334 'type' => 'imsobv2p1', 335 'discoveryurl' => null, 336 'hasmappingfields' => false, 337 'baseurl' => null, 338 'expectedexception' => \moodle_exception::class, 339 ], 340 'Microsoft' => [ 341 'type' => 'microsoft', 342 ], 343 'Facebook' => [ 344 'type' => 'facebook', 345 ], 346 'NextCloud' => [ 347 'type' => 'nextcloud', 348 'discoveryurl' => null, 349 'hasmappingfields' => true, 350 'baseurl' => 'https://dummy.local/nextcloud/', 351 ], 352 'NextCloud with empty baseurl should return an exception' => [ 353 'type' => 'nextcloud', 354 'discoveryurl' => null, 355 'hasmappingfields' => true, 356 'baseurl' => null, 357 'expectedexception' => \moodle_exception::class, 358 ], 359 'Invalid type should return an exception' => [ 360 'type' => 'fictitious', 361 'discoveryurl' => null, 362 'hasmappingfields' => true, 363 'baseurl' => null, 364 'expectedexception' => \moodle_exception::class, 365 ], 366 ]; 367 } 368 369 /** 370 * Test for get all issuers. 371 */ 372 public function test_get_all_issuers() { 373 $this->resetAfterTest(); 374 $this->setAdminUser(); 375 $googleissuer = api::create_standard_issuer('google'); 376 api::create_standard_issuer('facebook'); 377 api::create_standard_issuer('microsoft'); 378 379 // Set Google issuer to be shown only on login page. 380 $record = $googleissuer->to_record(); 381 $record->showonloginpage = $googleissuer::LOGINONLY; 382 api::update_issuer($record); 383 384 $issuers = api::get_all_issuers(); 385 $this->assertCount(2, $issuers); 386 $expected = ['Microsoft', 'Facebook']; 387 $this->assertEqualsCanonicalizing($expected, [$issuers[0]->get_display_name(), $issuers[1]->get_display_name()]); 388 389 $issuers = api::get_all_issuers(true); 390 $this->assertCount(3, $issuers); 391 $expected = ['Google', 'Microsoft', 'Facebook']; 392 $this->assertEqualsCanonicalizing($expected, 393 [$issuers[0]->get_display_name(), $issuers[1]->get_display_name(), $issuers[2]->get_display_name()]); 394 } 395 396 /** 397 * Test for is available for login. 398 */ 399 public function test_is_available_for_login() { 400 $this->resetAfterTest(); 401 $this->setAdminUser(); 402 $googleissuer = api::create_standard_issuer('google'); 403 404 // Set Google issuer to be shown only on login page. 405 $record = $googleissuer->to_record(); 406 $record->showonloginpage = $googleissuer::LOGINONLY; 407 api::update_issuer($record); 408 409 $this->assertFalse($googleissuer->is_available_for_login()); 410 411 // Set a clientid and clientsecret. 412 $googleissuer->set('clientid', 'clientid'); 413 $googleissuer->set('clientsecret', 'secret'); 414 $googleissuer->update(); 415 416 $this->assertTrue($googleissuer->is_available_for_login()); 417 418 // Set showonloginpage to service only. 419 $googleissuer->set('showonloginpage', issuer::SERVICEONLY); 420 $googleissuer->update(); 421 422 $this->assertFalse($googleissuer->is_available_for_login()); 423 424 // Set showonloginpage to everywhere (service and login) and disable issuer. 425 $googleissuer->set('showonloginpage', issuer::EVERYWHERE); 426 $googleissuer->set('enabled', 0); 427 $googleissuer->update(); 428 429 $this->assertFalse($googleissuer->is_available_for_login()); 430 431 // Enable issuer. 432 $googleissuer->set('enabled', 1); 433 $googleissuer->update(); 434 435 $this->assertTrue($googleissuer->is_available_for_login()); 436 437 // Remove userinfo endpoint from issuer. 438 $endpoint = endpoint::get_record([ 439 'issuerid' => $googleissuer->get('id'), 440 'name' => 'userinfo_endpoint' 441 ]); 442 api::delete_endpoint($endpoint->get('id')); 443 444 $this->assertFalse($googleissuer->is_available_for_login()); 445 } 446 447 /** 448 * Data provider for test_get_internalfield_list and test_get_internalfields. 449 * 450 * @return array 451 */ 452 public function create_custom_profile_fields(): array { 453 return [ 454 'data' => 455 [ 456 'given' => [ 457 'Hobbies' => [ 458 'shortname' => 'hobbies', 459 'name' => 'Hobbies', 460 ] 461 ], 462 'expected' => [ 463 'Hobbies' => [ 464 'shortname' => 'hobbies', 465 'name' => 'Hobbies', 466 ] 467 ] 468 ], 469 [ 470 'given' => [ 471 'Billing' => [ 472 'shortname' => 'billingaddress', 473 'name' => 'Billing Address', 474 ], 475 'Payment' => [ 476 'shortname' => 'creditcardnumber', 477 'name' => 'Credit Card Number', 478 ] 479 ], 480 'expected' => [ 481 'Billing' => [ 482 'shortname' => 'billingaddress', 483 'name' => 'Billing Address', 484 ], 485 'Payment' => [ 486 'shortname' => 'creditcardnumber', 487 'name' => 'Credit Card Number', 488 ] 489 ] 490 ] 491 ]; 492 } 493 494 /** 495 * Test getting the list of internal fields. 496 * 497 * @dataProvider create_custom_profile_fields 498 * @covers ::get_internalfield_list 499 * @param array $given Categories and profile fields. 500 * @param array $expected Expected value. 501 */ 502 public function test_get_internalfield_list(array $given, array $expected): void { 503 $this->resetAfterTest(); 504 self::generate_custom_profile_fields($given); 505 506 $userfieldmapping = new user_field_mapping(); 507 $internalfieldlist = $userfieldmapping->get_internalfield_list(); 508 509 foreach ($expected as $category => $value) { 510 // Custom profile fields must exist. 511 $this->assertNotEmpty($internalfieldlist[$category]); 512 513 // Category must have the custom profile fields with expected value. 514 $this->assertEquals( 515 $internalfieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']], 516 $value['name'] 517 ); 518 } 519 } 520 521 /** 522 * Test getting the list of internal fields with flat array. 523 * 524 * @dataProvider create_custom_profile_fields 525 * @covers ::get_internalfields 526 * @param array $given Categories and profile fields. 527 * @param array $expected Expected value. 528 */ 529 public function test_get_internalfields(array $given, array $expected): void { 530 $this->resetAfterTest(); 531 self::generate_custom_profile_fields($given); 532 533 $userfieldmapping = new user_field_mapping(); 534 $internalfields = $userfieldmapping->get_internalfields(); 535 536 // Custom profile fields must exist. 537 foreach ($expected as $category => $value) { 538 $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $internalfields); 539 } 540 } 541 542 /** 543 * Test getting the list of empty external/custom profile fields. 544 * 545 * @covers ::get_internalfields 546 */ 547 public function test_get_empty_internalfield_list(): void { 548 549 // Get internal (profile) fields. 550 $userfieldmapping = new user_field_mapping(); 551 $internalfieldlist = $userfieldmapping->get_internalfields(); 552 553 // Get user fields. 554 $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); 555 556 // Internal fields and user fields must exact same. 557 $this->assertEquals($userfields, $internalfieldlist); 558 } 559 560 /** 561 * Test getting Return the list of profile fields. 562 * 563 * @dataProvider create_custom_profile_fields 564 * @covers ::get_profile_field_list 565 * @param array $given Categories and profile fields. 566 * @param array $expected Expected value. 567 */ 568 public function test_get_profile_field_list(array $given, array $expected): void { 569 $this->resetAfterTest(); 570 self::generate_custom_profile_fields($given); 571 572 $profilefieldlist = get_profile_field_list(); 573 574 foreach ($expected as $category => $value) { 575 $this->assertEquals( 576 $profilefieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']], 577 $value['name'] 578 ); 579 } 580 } 581 582 /** 583 * Test getting the list of valid custom profile user fields. 584 * 585 * @dataProvider create_custom_profile_fields 586 * @covers ::get_profile_field_names 587 * @param array $given Categories and profile fields. 588 * @param array $expected Expected value. 589 */ 590 public function test_get_profile_field_names(array $given, array $expected): void { 591 $this->resetAfterTest(); 592 self::generate_custom_profile_fields($given); 593 594 $profilefieldnames = get_profile_field_names(); 595 596 // Custom profile fields must exist. 597 foreach ($expected as $category => $value) { 598 $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $profilefieldnames); 599 } 600 } 601 602 /** 603 * Generate data into DB for Testing getting user fields mapping. 604 * 605 * @param array $given Categories and profile fields. 606 */ 607 private function generate_custom_profile_fields(array $given): void { 608 // Create a profile category and the profile fields. 609 foreach ($given as $category => $value) { 610 $customprofilefieldcategory = ['name' => $category, 'sortorder' => 1]; 611 $category = $this->getDataGenerator()->create_custom_profile_field_category($customprofilefieldcategory); 612 $this->getDataGenerator()->create_custom_profile_field( 613 ['shortname' => $value['shortname'], 614 'name' => $value['name'], 615 'categoryid' => $category->id, 616 'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]); 617 } 618 } 619 620 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body