See Release Notes
Long Term Support Release
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 use enrol_lti\helper; 18 use enrol_lti\local\ltiadvantage\entity\application_registration; 19 use enrol_lti\local\ltiadvantage\repository\application_registration_repository; 20 use enrol_lti\local\ltiadvantage\repository\context_repository; 21 use enrol_lti\local\ltiadvantage\repository\deployment_repository; 22 use enrol_lti\local\ltiadvantage\repository\resource_link_repository; 23 use enrol_lti\local\ltiadvantage\repository\user_repository; 24 use enrol_lti\local\ltiadvantage\service\tool_launch_service; 25 use Packback\Lti1p3\LtiMessageLaunch; 26 27 /** 28 * Parent class for LTI Advantage tests, providing environment setup and mock user launches. 29 * 30 * @package enrol_lti 31 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 abstract class lti_advantage_testcase extends \advanced_testcase { 35 36 /** @var string the default issuer for tests extending this class. */ 37 protected $issuer = 'https://lms.example.org'; 38 39 /** 40 * Helper to return a user which has been bound to the LTI credentials provided and is deemed a valid linked user. 41 * 42 * @param string $sub the subject id string 43 * @param array $migrationclaiminfo mocked migration claim information, allowing the mock auth to bind to an existing user. 44 * @return stdClass the user record. 45 */ 46 protected function lti_advantage_user_authenticates(string $sub, array $migrationclaiminfo = []): \stdClass { 47 $auth = get_auth_plugin('lti'); 48 49 $mockjwt = [ 50 'iss' => $this->issuer, 51 'sub' => $sub, 52 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1222', // Must match deployment in create_test_env. 53 'aud' => '123', // Must match registration in create_test_environment. 54 'exp' => time() + 60, 55 'nonce' => 'some-nonce-value-123', 56 'given_name' => 'John', 57 'family_name' => 'Smith', 58 'email' => 'smithj@example.org' 59 ]; 60 if (!empty($migrationclaiminfo)) { 61 if (isset($migrationclaiminfo['consumer_key'])) { 62 $base = [ 63 $migrationclaiminfo['consumer_key'], 64 $mockjwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], 65 $mockjwt['iss'], 66 $mockjwt['aud'], 67 $mockjwt['exp'], 68 $mockjwt['nonce'] 69 ]; 70 $basestring = implode('&', $base); 71 72 $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 73 'oauth_consumer_key' => $migrationclaiminfo['consumer_key'], 74 ]; 75 76 if (isset($migrationclaiminfo['signing_secret'])) { 77 $sig = base64_encode(hash_hmac('sha256', $basestring, $migrationclaiminfo['signing_secret'])); 78 $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; 79 } 80 } 81 82 $claimprops = ['user_id', 'context_id', 'tool_consumer_instance_guid', 'resource_link_id']; 83 foreach ($claimprops as $prop) { 84 if (!empty($migrationclaiminfo[$prop])) { 85 $mockjwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'][$prop] = 86 $migrationclaiminfo[$prop]; 87 } 88 } 89 } 90 91 $secrets = !empty($migrationclaiminfo['signing_secret']) ? [$migrationclaiminfo['signing_secret']] : []; 92 return $auth->find_or_create_user_from_launch($mockjwt, false, $secrets); 93 } 94 95 /** 96 * Get a list of users ready for use with mock launches by providing an array of user ids. 97 * 98 * @param array $ids the platform user_ids for the users. 99 * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). 100 * @param string $role the LTI role to include in the user data. 101 * @return array the users list. 102 */ 103 protected function get_mock_launch_users_with_ids(array $ids, bool $includepicture = false, 104 string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'): array { 105 106 $users = []; 107 foreach ($ids as $id) { 108 $user = [ 109 'user_id' => $id, 110 'given_name' => 'Firstname' . $id, 111 'family_name' => 'Surname' . $id, 112 'email' => "firstname.surname{$id}@lms.example.org", 113 'roles' => [$role] 114 ]; 115 if ($includepicture) { 116 $user['picture'] = $this->getExternalTestFileUrl('/test.jpg'); 117 } 118 $users[] = $user; 119 } 120 return $users; 121 } 122 123 /** 124 * Get a mock LtiMessageLaunch object, as if a user had launched from a resource link in the platform. 125 * 126 * @param \stdClass $resource the resource record, allowing the mock to generate a link to this. 127 * @param array $mockuser the user on the platform who is performing the launch. 128 * @param string|null $resourcelinkid the id of resource link in the platform, if desired. 129 * @param array|null $ags array representing the lti-ags claim info. Pass null to omit, empty array to use a default. 130 * @param bool $nrps whether to include a mock NRPS claim or not. 131 * @param array|null $migrationclaiminfo contains consumer key, secret and any fields which are sent in the claim. 132 * @param array|null $customparams an array of custom params to send, or null to just use defaults. 133 * @param mixed $aud the array or string value of aud to use in the mock launch data. 134 * @return LtiMessageLaunch the mock launch object with test launch data. 135 */ 136 protected function get_mock_launch(\stdClass $resource, array $mockuser, 137 ?string $resourcelinkid = null, ?array $ags = [], bool $nrps = true, ?array $migrationclaiminfo = null, 138 ?array $customparams = null, $aud = '123'): LtiMessageLaunch { 139 140 $mocklaunch = $this->getMockBuilder(LtiMessageLaunch::class) 141 ->onlyMethods(['getLaunchData']) 142 ->disableOriginalConstructor() 143 ->getMock(); 144 $mocklaunch->expects($this->any()) 145 ->method('getLaunchData') 146 ->will($this->returnCallback( 147 function() 148 use ($resource, $mockuser, $resourcelinkid, $migrationclaiminfo, $ags, $nrps, $customparams, $aud) { 149 // This simulates the data in the jwt['body'] of a real resource link launch. 150 // Real launches would of course have this data and authenticity of the user verified. 151 $rltitle = $resourcelinkid ? "Resource link $resourcelinkid in platform" : "Resource link in platform"; 152 $rlid = $resourcelinkid ?: '12345'; 153 $data = [ 154 'iss' => $this->issuer, // Must match registration in create_test_environment. 155 'aud' => $aud, // Must match registration in create_test_environment. 156 'sub' => $mockuser['user_id'], // User id on the platform site. 157 'exp' => time() + 60, 158 'nonce' => 'some-nonce-value-123', 159 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1', // Must match registration. 160 'https://purl.imsglobal.org/spec/lti/claim/roles' => 161 $mockuser['roles'] ?? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'], 162 'https://purl.imsglobal.org/spec/lti/claim/resource_link' => [ 163 'title' => $rltitle, 164 'id' => $rlid, // Arbitrary, will be mapped to the user during resource link launch. 165 ], 166 "https://purl.imsglobal.org/spec/lti/claim/context" => [ 167 "id" => "context-id-12345", 168 "label" => "ITS 123", 169 "title" => "ITS 123 Machine Learning", 170 "type" => ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"] 171 ], 172 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri' => 173 'https://this-moodle-tool.example.org/context/24/resource/14', 174 'given_name' => $mockuser['given_name'], 175 'family_name' => $mockuser['family_name'], 176 'email' => $mockuser['email'], 177 ]; 178 179 if (!is_null($customparams)) { 180 $data['https://purl.imsglobal.org/spec/lti/claim/custom'] = $customparams; 181 } else { 182 $data['https://purl.imsglobal.org/spec/lti/claim/custom'] = [ 183 'id' => $resource->uuid, 184 ]; 185 } 186 187 if (is_array($ags)) { 188 if (empty($ags)) { 189 $agsclaim = [ 190 "scope" => [ 191 "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", 192 "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", 193 "https://purl.imsglobal.org/spec/lti-ags/scope/score" 194 ], 195 "lineitems" => "https://platform.example.com/10/lineitems/", 196 "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" 197 ]; 198 } else { 199 $agsclaim = $ags; 200 } 201 $data["https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"] = $agsclaim; 202 } 203 204 if ($nrps) { 205 $data['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] = [ 206 'context_memberships_url' => 'https://lms.example.org/context/24/memberships', 207 'service_versions' => ['2.0'] 208 ]; 209 } 210 211 if (!empty($mockuser['picture'])) { 212 $data['picture'] = $mockuser['picture']; 213 } 214 215 if ($migrationclaiminfo) { 216 if (isset($migrationclaiminfo['consumer_key'])) { 217 $base = [ 218 $migrationclaiminfo['consumer_key'], 219 $data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], 220 $data['iss'], 221 $data['aud'], 222 $data['exp'], 223 $data['nonce'] 224 ]; 225 $basestring = implode('&', $base); 226 227 $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 228 'oauth_consumer_key' => $migrationclaiminfo['consumer_key'], 229 ]; 230 231 if (isset($migrationclaiminfo['signing_secret'])) { 232 $sig = base64_encode(hash_hmac('sha256', $basestring, $migrationclaiminfo['signing_secret'])); 233 $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; 234 } 235 } 236 237 $claimprops = ['user_id', 'context_id', 'tool_consumer_instance_guid', 'resource_link_id']; 238 foreach ($claimprops as $prop) { 239 if (!empty($migrationclaiminfo[$prop])) { 240 $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'][$prop] = 241 $migrationclaiminfo[$prop]; 242 } 243 } 244 } 245 return $data; 246 } 247 )); 248 249 return $mocklaunch; 250 } 251 252 /** 253 * Sets up and returns a test course, including LTI-published resources, ready for testing. 254 * 255 * @param bool $enableauthplugin whether to enable the auth plugin during setup. 256 * @param bool $enableenrolplugin whether to enable the enrol plugin during setup. 257 * @param bool $membersync whether or not the published resource support membership sync with the platform. 258 * @param int $membersyncmode the mode of member sync to set up on the shared resource. 259 * @param bool $gradesync whether or not to enabled gradesync on the published resources. 260 * @param bool $gradesynccompletion whether or not to require gradesynccompletion on the published resources. 261 * @param int $enrolstartdate the unix time when the enrolment starts, or 0 for no start time. 262 * @param int $provisioningmodeinstructor the teacher provisioning mode for all created resources, 0 for default (prompt). 263 * @param int $provisioningmodelearner the student provisioning mode for all created resources, 0 for default (auto). 264 * @return array array of objects for use in individual tests; courses, tools. 265 */ 266 protected function create_test_environment(bool $enableauthplugin = true, bool $enableenrolplugin = true, 267 bool $membersync = true, int $membersyncmode = helper::MEMBER_SYNC_ENROL_AND_UNENROL, 268 bool $gradesync = true, bool $gradesynccompletion = false, int $enrolstartdate = 0, int $provisioningmodeinstructor = 0, 269 int $provisioningmodelearner = 0): array { 270 271 global $CFG; 272 require_once($CFG->libdir . '/completionlib.php'); 273 require_once($CFG->dirroot . '/auth/lti/auth.php'); 274 275 if ($enableauthplugin) { 276 $this->enable_auth(); 277 } 278 if ($enableenrolplugin) { 279 $this->enable_enrol(); 280 } 281 282 // Set up the registration and deployment. 283 $reg = application_registration::create( 284 'Example LMS application', 285 'a2c94a2c94', 286 new moodle_url($this->issuer), 287 '123', 288 new moodle_url('https://example.org/authrequesturl'), 289 new moodle_url('https://example.org/jwksurl'), 290 new moodle_url('https://example.org/accesstokenurl') 291 ); 292 $regrepo = new application_registration_repository(); 293 $reg = $regrepo->save($reg); 294 $deployment = $reg->add_tool_deployment('My tool deployment', '1'); 295 $deploymentrepo = new deployment_repository(); 296 $deployment = $deploymentrepo->save($deployment); 297 298 $generator = $this->getDataGenerator(); 299 $course = $generator->create_course(['enablecompletion' => 1]); 300 301 // Create a module and publish it. 302 $mod = $generator->create_module('assign', ['course' => $course->id, 'grade' => 100, 'completionsubmit' => 1, 303 'completion' => COMPLETION_TRACKING_AUTOMATIC]); 304 $tooldata = [ 305 'cmid' => $mod->cmid, 306 'courseid' => $course->id, 307 'membersyncmode' => $membersyncmode, 308 'membersync' => $membersync, 309 'gradesync' => $gradesync, 310 'gradesynccompletion' => $gradesynccompletion, 311 'ltiversion' => 'LTI-1p3', 312 'enrolstartdate' => $enrolstartdate, 313 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 314 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY 315 ]; 316 $tool = $generator->create_lti_tool((object)$tooldata); 317 $tool = helper::get_lti_tool($tool->id); 318 319 // Create a second module and publish it. 320 $mod = $generator->create_module('assign', ['course' => $course->id, 'grade' => 100, 'completionsubmit' => 1, 321 'completion' => COMPLETION_TRACKING_AUTOMATIC]); 322 $tooldata = [ 323 'cmid' => $mod->cmid, 324 'courseid' => $course->id, 325 'membersyncmode' => $membersyncmode, 326 'membersync' => $membersync, 327 'gradesync' => $gradesync, 328 'gradesynccompletion' => $gradesynccompletion, 329 'ltiversion' => 'LTI-1p3', 330 'enrolstartdate' => $enrolstartdate, 331 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 332 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY 333 ]; 334 $tool2 = $generator->create_lti_tool((object)$tooldata); 335 $tool2 = helper::get_lti_tool($tool2->id); 336 337 // Create a course and publish it. 338 $tooldata = [ 339 'courseid' => $course->id, 340 'membersyncmode' => $membersyncmode, 341 'membersync' => $membersync, 342 'gradesync' => $gradesync, 343 'gradesynccompletion' => $gradesynccompletion, 344 'ltiversion' => 'LTI-1p3', 345 'enrolstartdate' => $enrolstartdate, 346 'provisioningmodeinstructor' => $provisioningmodeinstructor ?: auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING, 347 'provisioningmodelearner' => $provisioningmodelearner ?: auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY 348 ]; 349 $tool3 = $generator->create_lti_tool((object)$tooldata); 350 $tool3 = helper::get_lti_tool($tool3->id); 351 352 return [$course, $tool, $tool2, $tool3, $reg, $deployment]; 353 } 354 355 /** 356 * Enable auth_lti plugin. 357 */ 358 protected function enable_auth() { 359 $class = \core_plugin_manager::resolve_plugininfo_class('auth'); 360 $class::enable_plugin('lti', true); 361 } 362 363 /** 364 * Enable enrol_lti plugin. 365 */ 366 protected function enable_enrol() { 367 $class = \core_plugin_manager::resolve_plugininfo_class('enrol'); 368 $class::enable_plugin('lti', true); 369 } 370 371 /** 372 * Helper to get a tool_launch_service instance. 373 * 374 * @return tool_launch_service the instance. 375 */ 376 protected function get_tool_launch_service(): tool_launch_service { 377 return new tool_launch_service( 378 new deployment_repository(), 379 new application_registration_repository(), 380 new resource_link_repository(), 381 new user_repository(), 382 new context_repository() 383 ); 384 } 385 386 /** 387 * Set up data representing a several published legacy tools, including tool records, tool consumer maps and a user. 388 * 389 * @param stdClass $course the course in which to create the tools. 390 * @param array $legacydata array containing user id, consumer key and tool secrets for creation of records. 391 * @return array array containing [tool1record, tool2record, consumerrecord, userrecord]. 392 */ 393 protected function setup_legacy_data(\stdClass $course, array $legacydata): array { 394 // Legacy data: create a consumer record. 395 global $DB; 396 $generator = $this->getDataGenerator(); 397 $now = time(); 398 $consumerrecord = (object) [ 399 'name' => 'consumer name', 400 'consumerkey256' => $legacydata['consumer_key'], 401 'secret' => '0987654321fff', 402 'protected' => true, 403 'enabled' => true, 404 'created' => $now, 405 'updated' => $now, 406 ]; 407 $consumerrecord->id = $DB->insert_record('enrol_lti_lti2_consumer', $consumerrecord); 408 409 // Legacy data: create some modules and publish them as tools, using different secrets, over LTI 1.1. 410 $tools = []; 411 $toolconsumermaprecords = []; 412 foreach ($legacydata['tools'] as $tool) { 413 $mod = $generator->create_module('assign', ['course' => $course->id]); 414 $tooldata = [ 415 'cmid' => $mod->cmid, 416 'courseid' => $course->id, 417 'membersyncmode' => helper::MEMBER_SYNC_ENROL_AND_UNENROL, 418 'membersync' => false, 419 'ltiversion' => 'LTI-1p0/LTI-2p0', 420 'secret' => $tool['secret'] 421 ]; 422 $legacytool = $generator->create_lti_tool((object)$tooldata); 423 $tools[] = $legacytool; 424 $toolconsumermaprecords[] = ['toolid' => $legacytool->id, 'consumerid' => $consumerrecord->id]; 425 } 426 427 // Legacy data: create the tool consumer map, which is created during launches. 428 $DB->insert_records('enrol_lti_tool_consumer_map', $toolconsumermaprecords); 429 430 // Legacy data: create the user who launched the tools over LTI 1.1. 431 if (!empty($legacydata['users'])) { 432 $legacyusers = []; 433 foreach ($legacydata['users'] as $legacyuser) { 434 $legacyusers[] = $generator->create_user([ 435 'username' => helper::create_username($consumerrecord->consumerkey256, $legacyuser['user_id']), 436 'auth' => 'lti', 437 ]); 438 } 439 } 440 441 return [$tools, $consumerrecord, $legacyusers ?? null]; 442 } 443 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body