Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
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 enrol_lti\local\ltiadvantage\task; 18 19 use enrol_lti\helper; 20 use Packback\Lti1p3\LtiAssignmentsGradesService; 21 use Packback\Lti1p3\LtiGrade; 22 use Packback\Lti1p3\LtiLineitem; 23 24 defined('MOODLE_INTERNAL') || die(); 25 26 require_once (__DIR__ . '/../lti_advantage_testcase.php'); 27 28 /** 29 * Tests for the enrol_lti\local\ltiadvantage\task\sync_grades scheduled task. 30 * 31 * @package enrol_lti 32 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_grades 35 */ 36 class sync_grades_test extends \lti_advantage_testcase { 37 /** 38 * Get a task which has a mocked ags instance injected, meaning no real calls will be made to the platform. 39 * 40 * This allows us to test the behaviour of the task (in terms of which users are in scope and which grades are sent) 41 * without needing to deal with any auth. 42 * 43 * @param string $statuscode the HTTP status code to simulate. 44 * @param bool $mockexception whether to simulate an exception during the service call or not. 45 * @return sync_grades instance of the task with a mocked ags instance inside. 46 */ 47 protected function get_task_with_mocked_grade_service($statuscode = '200', $mockexception = false): sync_grades { 48 $mockgradeservice = $this->getMockBuilder(LtiAssignmentsGradesService::class) 49 ->disableOriginalConstructor() 50 ->onlyMethods(['putGrade']) 51 ->getMock(); 52 $mockgradeservice->method('putGrade')->willReturnCallback(function() use ($statuscode, $mockexception) { 53 if ($mockexception) { 54 throw new \Exception(); 55 } 56 return ['headers' => ['httpstatus' => "HTTP/2 $statuscode OK"], 'body' => '', 'status' => $statuscode]; 57 }); 58 // Get a mock task, with the method 'get_ags()' mocked to return the mocked AGS instance. 59 $mocktask = $this->getMockBuilder(sync_grades::class) 60 ->onlyMethods(['get_ags']) 61 ->getMock(); 62 $mocktask->method('get_ags')->willReturn($mockgradeservice); 63 return $mocktask; 64 } 65 66 /** 67 * Helper function to set a grade for a user. 68 * 69 * @param int $userid the id of the user being graded. 70 * @param float $grade the grade value, out of 100, to set for the user. 71 * @param \stdClass $resource the published resource object. 72 * @return float the fractional grade value expected to be used during sync. 73 */ 74 protected function set_user_grade_for_resource(int $userid, float $grade, \stdClass $resource): float { 75 76 global $CFG; 77 require_once($CFG->libdir . '/accesslib.php'); 78 require_once($CFG->libdir . '/gradelib.php'); 79 $context = \context::instance_by_id($resource->contextid); 80 81 if ($context->contextlevel == CONTEXT_COURSE) { 82 $gi = \grade_item::fetch_course_item($resource->courseid); 83 } else if ($context->contextlevel == CONTEXT_MODULE) { 84 $cm = get_coursemodule_from_id('assign', $context->instanceid); 85 86 $gi = \grade_item::fetch([ 87 'itemtype' => 'mod', 88 'itemmodule' => 'assign', 89 'iteminstance' => $cm->instance, 90 'courseid' => $resource->courseid 91 ]); 92 } 93 94 if ($ggrade = \grade_grade::fetch(['itemid' => $gi->id, 'userid' => $userid])) { 95 $ggrade->finalgrade = $grade; 96 $ggrade->rawgrade = $grade; 97 $ggrade->update(); 98 } else { 99 $ggrade = new \grade_grade(); 100 $ggrade->itemid = $gi->id; 101 $ggrade->userid = $userid; 102 $ggrade->rawgrade = $grade; 103 $ggrade->finalgrade = $grade; 104 $ggrade->rawgrademax = 100; 105 $ggrade->rawgrademin = 0; 106 $ggrade->timecreated = time(); 107 $ggrade->timemodified = time(); 108 $ggrade->insert(); 109 } 110 return floatval($ggrade->finalgrade / $gi->grademax); 111 } 112 113 /** 114 * Helper to set the completion status for published course or module. 115 * 116 * @param \stdClass $resource the resource - either a course or module. 117 * @param int $userid the id of the user to override the completion status for. 118 * @param bool $complete whether the resource is deemed complete or not. 119 */ 120 protected function override_resource_completion_status_for_user(\stdClass $resource, int $userid, 121 bool $complete): void { 122 123 global $CFG; 124 require_once($CFG->libdir . '/accesslib.php'); 125 require_once($CFG->libdir . '/completionlib.php'); 126 require_once($CFG->libdir . '/datalib.php'); 127 $this->setAdminUser(); 128 $context = \context::instance_by_id($resource->contextid); 129 $completion = new \completion_info(get_course($resource->courseid)); 130 if ($context->contextlevel == CONTEXT_COURSE) { 131 $ccompletion = new \completion_completion(['userid' => $userid, 'course' => $resource->courseid]); 132 if ($complete) { 133 $ccompletion->mark_complete(); 134 } else { 135 $completion->clear_criteria(); 136 } 137 } else if ($context->contextlevel == CONTEXT_MODULE) { 138 $completion->update_state( 139 get_coursemodule_from_id('assign', $context->instanceid), 140 $complete ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE, 141 $userid, 142 true 143 ); 144 } 145 } 146 147 /** 148 * Test confirming task name. 149 * 150 * @covers ::get_name 151 */ 152 public function test_get_name() { 153 $this->assertEquals(get_string('tasksyncgrades', 'enrol_lti'), (new sync_grades())->get_name()); 154 } 155 156 /** 157 * Test the sync grades task during several runs and for a series of grade changes. 158 * 159 * @covers ::execute 160 */ 161 public function test_grade_sync_chronological_syncs() { 162 $this->resetAfterTest(); 163 164 [$course, $resource] = $this->create_test_environment(); 165 $launchservice = $this->get_tool_launch_service(); 166 $task = $this->get_task_with_mocked_grade_service(); 167 168 // Launch the resource for an instructor which will create the domain objects needed for service calls. 169 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 170 $instructoruser = $this->getDataGenerator()->create_user(); 171 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 172 173 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 174 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 175 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 176 177 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 178 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); 179 $student1user = $this->getDataGenerator()->create_user(); 180 $student2user = $this->getDataGenerator()->create_user(); 181 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 182 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 183 184 // Grade student1 only. 185 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 186 187 // Sync and verify that only student1's grade is sent. 188 ob_start(); 189 $task->execute(); 190 $ob = ob_get_contents(); 191 ob_end_clean(); 192 $expectedtraces = [ 193 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 194 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 195 "'$course->id'.", 196 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 197 "'$resource->id' and the course '$course->id' was sent.", 198 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 199 "'$course->id'.", 200 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 201 "Processed 3 users; sent 1 grades." 202 ]; 203 foreach ($expectedtraces as $expectedtrace) { 204 $this->assertStringContainsString($expectedtrace, $ob); 205 } 206 207 // Sync again, verifying no grades are sent because nothing has changed. 208 ob_start(); 209 $task->execute(); 210 $ob = ob_get_contents(); 211 ob_end_clean(); 212 $expectedtraces = [ 213 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 214 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 215 "'$course->id'.", 216 "Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ". 217 "'$course->id' was not sent as the grades are the same.", 218 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 219 "'$course->id'.", 220 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 221 "Processed 3 users; sent 0 grades." 222 ]; 223 foreach ($expectedtraces as $expectedtrace) { 224 $this->assertStringContainsString($expectedtrace, $ob); 225 } 226 227 // Change student1's grade and add a grade for student2. 228 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource); 229 $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource); 230 231 // Sync again, verifying both grade changes are sent. 232 ob_start(); 233 $task->execute(); 234 $ob = ob_get_contents(); 235 ob_end_clean(); 236 237 $expectedtraces = [ 238 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 239 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 240 "'$course->id'.", 241 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 242 "'$resource->id' and the course '$course->id' was sent.", 243 "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". 244 "'$resource->id' and the course '$course->id' was sent.", 245 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 246 "Processed 3 users; sent 2 grades." 247 ]; 248 foreach ($expectedtraces as $expectedtrace) { 249 $this->assertStringContainsString($expectedtrace, $ob); 250 } 251 } 252 253 /** 254 * Test a grade sync when there are more than one resource link for the resource. 255 * 256 * @covers ::execute 257 */ 258 public function test_grade_sync_multiple_resource_links() { 259 $this->resetAfterTest(); 260 261 [$course, $resource] = $this->create_test_environment(); 262 $launchservice = $this->get_tool_launch_service(); 263 $task = $this->get_task_with_mocked_grade_service(); 264 265 // Launch the resource first for an instructor using the default resource link in the platform. 266 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 267 $instructoruser = $this->getDataGenerator()->create_user(); 268 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 269 270 // Launch again as the instructor, this time from a different resource link in the platform. 271 $teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2'); 272 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch2); 273 274 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 275 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 276 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 277 278 $student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]); 279 $student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]); 280 $student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2'); 281 $student1user = $this->getDataGenerator()->create_user(); 282 $student2user = $this->getDataGenerator()->create_user(); 283 [$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch); 284 [$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch); 285 $launchservice->user_launches_tool($student1user, $student1reslink2launch); 286 287 // Grade student1 only. 288 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 289 290 // Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links. 291 ob_start(); 292 $task->execute(); 293 $ob = ob_get_contents(); 294 ob_end_clean(); 295 296 $expectedtraces = [ 297 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 298 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 299 "'$course->id'.", 300 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 301 "'$resource->id' and the course '$course->id' was sent.", 302 "Found 2 resource link(s) for the user '$student1id', for the resource ". 303 "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.", 304 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 305 "'$course->id'.", 306 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 307 "Processed 3 users; sent 1 grades." 308 ]; 309 foreach ($expectedtraces as $expectedtrace) { 310 $this->assertStringContainsString($expectedtrace, $ob); 311 } 312 313 // Verify that the grade was reported as being synced twice - once for each resource link. 314 $expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ". 315 "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n". 316 "Processing resource link '.*'.\n". 317 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 318 "'$resource->id' and the course '$course->id' was sent.\n". 319 "Processing resource link '.*'.\n". 320 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 321 "'$resource->id' and the course '$course->id' was sent./"; 322 $this->assertMatchesRegularExpression($expected, $ob); 323 } 324 325 /** 326 * Test grade sync when the resource has syncgrades disabled. 327 * 328 * @covers ::execute 329 */ 330 public function test_sync_grades_gradesync_disabled() { 331 $this->resetAfterTest(); 332 [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, 333 false); 334 $launchservice = $this->get_tool_launch_service(); 335 336 // Launch the resource for an instructor which will create the domain objects needed for service calls. 337 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 338 $instructoruser = $this->getDataGenerator()->create_user(); 339 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 340 341 $task = $this->get_task_with_mocked_grade_service(); 342 $this->expectOutputRegex('/Skipping task - There are no resources with grade sync enabled./'); 343 $task->execute(); 344 } 345 346 /** 347 * Test the grade sync task when the auth_lti plugin is disabled. 348 * 349 * @covers ::execute 350 */ 351 public function test_sync_grades_auth_plugin_disabled() { 352 $this->resetAfterTest(); 353 [$course, $resource] = $this->create_test_environment(false); 354 $launchservice = $this->get_tool_launch_service(); 355 356 // Launch the resource for an instructor which will create the domain objects needed for service calls. 357 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 358 $instructoruser = $this->getDataGenerator()->create_user(); 359 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 360 361 $task = $this->get_task_with_mocked_grade_service(); 362 $this->expectOutputRegex('/Skipping task - ' . 363 get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')) . '/'); 364 $task->execute(); 365 } 366 367 /** 368 * Test the grade sync task when the enrol_lti plugin is disabled. 369 * 370 * @covers ::execute 371 */ 372 public function test_sync_grades_enrol_plugin_disabled() { 373 $this->resetAfterTest(); 374 [$course, $resource] = $this->create_test_environment(true, false); 375 $launchservice = $this->get_tool_launch_service(); 376 377 // Launch the resource for an instructor which will create the domain objects needed for service calls. 378 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 379 $instructoruser = $this->getDataGenerator()->create_user(); 380 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 381 382 $task = $this->get_task_with_mocked_grade_service(); 383 $this->expectOutputRegex('/Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti') . '/'); 384 $task->execute(); 385 } 386 387 /** 388 * Test the grade sync task when the launch data doesn't include the AGS support. 389 * 390 * @covers ::execute 391 */ 392 public function test_sync_grades_no_service_endpoint() { 393 $this->resetAfterTest(); 394 [$course, $resource] = $this->create_test_environment(); 395 $launchservice = $this->get_tool_launch_service(); 396 397 // Launch the resource for an instructor which will create the domain objects needed for service calls. 398 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 399 null, null); 400 $instructoruser = $this->getDataGenerator()->create_user(); 401 [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 402 403 $task = $this->get_task_with_mocked_grade_service(); 404 $this->expectOutputRegex( 405 "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". 406 "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". 407 "course '$course->id'. Attempting to sync grades for all.\n". 408 "Processing resource link '.*'.\n". 409 "Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ". 410 "course '$course->id'.\n". 411 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". 412 "sent 0 grades./" 413 ); 414 $task->execute(); 415 } 416 417 /** 418 * Test syncing grades when the enrolment instance is disabled. 419 * 420 * @covers ::execute 421 */ 422 public function test_sync_grades_disabled_instance() { 423 $this->resetAfterTest(); 424 global $DB; 425 426 [$course, $resource, $resource2, $resource3] = $this->create_test_environment(); 427 428 // Disable resource 1. 429 $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED]; 430 $DB->update_record('enrol', $enrol); 431 432 // Delete the activity being shared by resource 2, leaving resource 2 disabled as a result. 433 $modcontext = \context::instance_by_id($resource2->contextid); 434 course_delete_module($modcontext->instanceid); 435 436 // Only the enabled resource 3 should sync grades. 437 $task = $this->get_task_with_mocked_grade_service(); 438 $this->expectOutputRegex( 439 "/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n". 440 "Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ". 441 "sent 0 grades.\n$/" 442 ); 443 $task->execute(); 444 } 445 446 /** 447 * Test the grade sync when the context has been deleted in between launch and when the grade sync task is run. 448 * 449 * @covers ::execute 450 */ 451 public function test_sync_grades_deleted_context() { 452 $this->resetAfterTest(); 453 global $DB; 454 455 [$course, $resource] = $this->create_test_environment(); 456 $launchservice = $this->get_tool_launch_service(); 457 458 // Launch the resource for an instructor which will create the domain objects needed for service calls. 459 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 460 $instructoruser = $this->getDataGenerator()->create_user(); 461 [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 462 463 // Delete the activity, then enable the enrolment method (it is disabled during activity deletion). 464 $modcontext = \context::instance_by_id($resource->contextid); 465 course_delete_module($modcontext->instanceid); 466 $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED]; 467 $DB->update_record('enrol', $enrol); 468 469 $task = $this->get_task_with_mocked_grade_service(); 470 $this->expectOutputRegex( 471 "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". 472 "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". 473 "course '$course->id'. Attempting to sync grades for all.\n". 474 "Processing resource link '.*'.\n". 475 "Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n". 476 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". 477 "sent 0 grades./" 478 ); 479 $task->execute(); 480 } 481 482 /** 483 * Test grade sync when completion is required for the activity before sync takes place. 484 * 485 * @covers ::execute 486 */ 487 public function test_sync_grades_completion_required() { 488 $this->resetAfterTest(); 489 global $CFG; 490 require_once($CFG->libdir . '/completionlib.php'); 491 492 [ 493 $course, 494 $resource, 495 $resource2, 496 $publishedcourse 497 ] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true); 498 $launchservice = $this->get_tool_launch_service(); 499 $task = $this->get_task_with_mocked_grade_service(); 500 501 // Launch the resource for an instructor which will create the domain objects needed for service calls. 502 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 503 $instructoruser = $this->getDataGenerator()->create_user(); 504 [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 505 506 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 507 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 508 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 509 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 510 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); 511 $student1user = $this->getDataGenerator()->create_user(); 512 $student2user = $this->getDataGenerator()->create_user(); 513 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 514 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 515 516 // Launch the published course as student2. 517 $student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456'); 518 $launchservice->user_launches_tool($student2user, $student2mockcourselaunch); 519 520 // Grade student1 in the assign resource. 521 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 522 523 // And student2 in the course resource. 524 $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse); 525 526 // Sync and verify that no grades are sent because resource and published course are both not yet complete. 527 ob_start(); 528 $task->execute(); 529 $ob = ob_get_contents(); 530 ob_end_clean(); 531 $expectedtraces = [ 532 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 533 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 534 "'$course->id'.", 535 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 536 "'$course->id'.", 537 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 538 "'$course->id'.", 539 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 540 "the course '$course->id'.", 541 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 542 "Processed 3 users; sent 0 grades." 543 ]; 544 foreach ($expectedtraces as $expectedtrace) { 545 $this->assertStringContainsString($expectedtrace, $ob); 546 } 547 548 // Complete the resource for student1. 549 $this->override_resource_completion_status_for_user($resource, $student1id, true); 550 551 // Run the sync again, this time confirming the grade for student1 is sent. 552 ob_start(); 553 $task->execute(); 554 $ob = ob_get_contents(); 555 ob_end_clean(); 556 $expectedtraces = [ 557 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 558 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 559 "'$course->id'.", 560 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 561 "'$resource->id' and the course '$course->id' was sent.", 562 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 563 "'$course->id'.", 564 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 565 "Processed 3 users; sent 1 grades.", 566 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 567 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 568 "the course '$course->id'.", 569 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 570 "Processed 1 users; sent 0 grades.", 571 ]; 572 foreach ($expectedtraces as $expectedtrace) { 573 $this->assertStringContainsString($expectedtrace, $ob); 574 } 575 576 // Fail completion for student1 and confirm no grade is sent, even despite it being changed. 577 $this->set_user_grade_for_resource($student1id, 33.3, $resource); 578 $this->override_resource_completion_status_for_user($resource, $student1id, false); 579 580 ob_start(); 581 $task->execute(); 582 $ob = ob_get_contents(); 583 ob_end_clean(); 584 $expectedtraces = [ 585 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 586 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 587 "'$course->id'.", 588 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 589 "'$course->id'.", 590 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 591 "'$course->id'.", 592 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 593 "Processed 3 users; sent 0 grades.", 594 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 595 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 596 "the course '$course->id'.", 597 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 598 "Processed 1 users; sent 0 grades.", 599 ]; 600 foreach ($expectedtraces as $expectedtrace) { 601 $this->assertStringContainsString($expectedtrace, $ob); 602 } 603 604 // Complete the course for student2 and verify the grade is now sent. 605 $this->override_resource_completion_status_for_user($publishedcourse, $student2id, true); 606 607 ob_start(); 608 $task->execute(); 609 $ob = ob_get_contents(); 610 ob_end_clean(); 611 $expectedtraces = [ 612 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 613 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 614 "'$course->id'.", 615 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 616 "'$course->id'.", 617 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 618 "'$course->id'.", 619 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 620 "Processed 3 users; sent 0 grades.", 621 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 622 "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". 623 "'$publishedcourse->id' and the course '$course->id' was sent.", 624 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 625 "Processed 1 users; sent 1 grades.", 626 627 ]; 628 foreach ($expectedtraces as $expectedtrace) { 629 $this->assertStringContainsString($expectedtrace, $ob); 630 } 631 632 // Mark the course as in progress again for student2 and verify any new grade changes are not sent. 633 $this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse); 634 $this->override_resource_completion_status_for_user($publishedcourse, $student2id, false); 635 636 ob_start(); 637 $task->execute(); 638 $ob = ob_get_contents(); 639 ob_end_clean(); 640 $expectedtraces = [ 641 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 642 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 643 "'$course->id'.", 644 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 645 "'$course->id'.", 646 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 647 "'$course->id'.", 648 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 649 "the course '$course->id'.", 650 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 651 "Processed 3 users; sent 0 grades." 652 ]; 653 foreach ($expectedtraces as $expectedtrace) { 654 $this->assertStringContainsString($expectedtrace, $ob); 655 } 656 } 657 658 /** 659 * Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code. 660 * 661 * @covers ::execute 662 */ 663 public function test_sync_grades_failed_service_call() { 664 $this->resetAfterTest(); 665 [$course, $resource] = $this->create_test_environment(); 666 $launchservice = $this->get_tool_launch_service(); 667 $task = $this->get_task_with_mocked_grade_service('200', true); 668 669 // Launch the resource for an instructor which will create the domain objects needed for service calls. 670 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 671 $instructoruser = $this->getDataGenerator()->create_user(); 672 [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 673 674 // Launch the resource for a student, creating the enrolment and allowing grading to take place. 675 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 676 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 677 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 678 $student1user = $this->getDataGenerator()->create_user(); 679 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 680 681 // Grade student1 in the assign resource. 682 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 683 684 // Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes. 685 ob_start(); 686 $task->execute(); 687 $ob = ob_get_contents(); 688 ob_end_clean(); 689 $expectedtraces = [ 690 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 691 "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 692 "'$resource->id' and the course '$course->id' failed to send.", 693 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 694 "Processed 2 users; sent 0 grades." 695 ]; 696 foreach ($expectedtraces as $expectedtrace) { 697 $this->assertStringContainsString($expectedtrace, $ob); 698 } 699 700 // Now run the sync again, this time with a bad http response code. 701 $task = $this->get_task_with_mocked_grade_service('400'); 702 ob_start(); 703 $task->execute(); 704 $ob = ob_get_contents(); 705 ob_end_clean(); 706 $expectedtraces = [ 707 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 708 "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 709 "'$resource->id' and the course '$course->id' failed to send.", 710 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 711 "Processed 2 users; sent 0 grades." 712 ]; 713 foreach ($expectedtraces as $expectedtrace) { 714 $this->assertStringContainsString($expectedtrace, $ob); 715 } 716 } 717 718 /** 719 * Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected. 720 * 721 * @covers ::execute 722 */ 723 public function test_sync_grades_coupled_lineitem() { 724 $this->resetAfterTest(); 725 726 [$course, $resource] = $this->create_test_environment(); 727 $launchservice = $this->get_tool_launch_service(); 728 729 // The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried. 730 $agsclaim = [ 731 "scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"], 732 "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" 733 ]; 734 735 // Launch the resource for an instructor which will create the domain objects needed for service calls. 736 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, 737 $agsclaim); 738 $instructoruser = $this->getDataGenerator()->create_user(); 739 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 740 741 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 742 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 743 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 744 745 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); 746 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); 747 $student1user = $this->getDataGenerator()->create_user(); 748 $student2user = $this->getDataGenerator()->create_user(); 749 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 750 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 751 752 // Grade student1 only. 753 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 754 755 // Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object. 756 $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); 757 $mockgradeservice->method('putGrade')->willReturnCallback(function() { 758 return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; 759 }); 760 $mockgradeservice->expects($this->never()) 761 ->method('findOrCreateLineitem'); 762 $mockgradeservice->expects($this->once()) 763 ->method('putGrade') 764 ->with($this->isInstanceOf(LtiGrade::class)); 765 $mocktask = $this->getMockBuilder(sync_grades::class) 766 ->onlyMethods(['get_ags']) 767 ->getMock(); 768 $mocktask->method('get_ags')->willReturn($mockgradeservice); 769 770 // Sync and verify that only student1's grade is sent. 771 ob_start(); 772 $mocktask->execute(); 773 $ob = ob_get_contents(); 774 ob_end_clean(); 775 $expectedtraces = [ 776 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 777 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 778 "'$course->id'.", 779 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 780 "'$resource->id' and the course '$course->id' was sent.", 781 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 782 "'$course->id'.", 783 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 784 "Processed 3 users; sent 1 grades." 785 ]; 786 foreach ($expectedtraces as $expectedtrace) { 787 $this->assertStringContainsString($expectedtrace, $ob); 788 } 789 } 790 791 /** 792 * Test the sync when only the lineitems URL is provided and when line item creation/query is expected. 793 * 794 * @covers ::execute 795 */ 796 public function test_sync_grades_none_or_many_lineitems() { 797 $this->resetAfterTest(); 798 799 [$course, $resource] = $this->create_test_environment(); 800 $launchservice = $this->get_tool_launch_service(); 801 802 // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items. 803 $agsclaim = [ 804 "scope" => [ 805 "https://purl.imsglobal.org/spec/lti-ags/scope/score", 806 "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", 807 ], 808 "lineitems" => "https://platform.example.com/10/lineitems" 809 ]; 810 811 // Launch the resource for an instructor which will create the domain objects needed for service calls. 812 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, 813 $agsclaim); 814 $instructoruser = $this->getDataGenerator()->create_user(); 815 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 816 817 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 818 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 819 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 820 821 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); 822 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); 823 $student1user = $this->getDataGenerator()->create_user(); 824 $student2user = $this->getDataGenerator()->create_user(); 825 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 826 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 827 828 // Grade student1 only. 829 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 830 831 // Mock task, asserting that line item creation takes place via a mock grade service object. 832 $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); 833 $mockgradeservice->method('putGrade')->willReturnCallback(function() { 834 return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; 835 }); 836 $mockgradeservice->expects($this->once()) 837 ->method('findOrCreateLineitem'); 838 $mockgradeservice->expects($this->once()) 839 ->method('putGrade') 840 ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class)); 841 $mocktask = $this->getMockBuilder(sync_grades::class) 842 ->onlyMethods(['get_ags']) 843 ->getMock(); 844 $mocktask->method('get_ags')->willReturn($mockgradeservice); 845 846 // Sync and verify that only student1's grade is sent. 847 ob_start(); 848 $mocktask->execute(); 849 $ob = ob_get_contents(); 850 ob_end_clean(); 851 $expectedtraces = [ 852 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 853 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 854 "'$course->id'.", 855 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 856 "'$resource->id' and the course '$course->id' was sent.", 857 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 858 "'$course->id'.", 859 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 860 "Processed 3 users; sent 1 grades." 861 ]; 862 foreach ($expectedtraces as $expectedtrace) { 863 $this->assertStringContainsString($expectedtrace, $ob); 864 } 865 } 866 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body