See Release Notes
Long Term Support Release
Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 /** 149 * Data provider for test_grade_sync_positive_case. 150 * 151 * @return array 152 */ 153 public static function grade_sync_positive_cases(): array { 154 return [ 155 [200], 156 [201], 157 [202], 158 [204], 159 ]; 160 } 161 162 /** 163 * Test the sync grades task works correct when platform responses with given status code. 164 * 165 * @covers ::execute 166 * @param string $statuscode the response status code with which the job should work correctly 167 * @dataProvider grade_sync_positive_cases 168 */ 169 public function test_grade_sync_positive_case($statuscode): void { 170 $this->resetAfterTest(); 171 172 [$course, $resource] = $this->create_test_environment(); 173 $launchservice = $this->get_tool_launch_service(); 174 $task = $this->get_task_with_mocked_grade_service($statuscode); 175 176 // Launch the resource for an instructor which will create the domain objects needed for service calls. 177 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 178 $instructoruser = $this->getDataGenerator()->create_user(); 179 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 180 181 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 182 $studentusers = $this->get_mock_launch_users_with_ids(['2'], false, 183 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 184 185 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 186 $student1user = $this->getDataGenerator()->create_user(); 187 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 188 189 // Grade student1. 190 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 191 192 // Sync and verify that only student1's grade is sent. 193 ob_start(); 194 $task->execute(); 195 $ob = ob_get_contents(); 196 ob_end_clean(); 197 $expectedtraces = [ 198 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 199 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 200 "'$course->id'.", 201 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 202 "'$resource->id' and the course '$course->id' was sent.", 203 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 204 "Processed 2 users; sent 1 grades." 205 ]; 206 foreach ($expectedtraces as $expectedtrace) { 207 $this->assertStringContainsString($expectedtrace, $ob); 208 } 209 } 210 211 /** 212 * Test confirming task name. 213 * 214 * @covers ::get_name 215 */ 216 public function test_get_name() { 217 $this->assertEquals(get_string('tasksyncgrades', 'enrol_lti'), (new sync_grades())->get_name()); 218 } 219 220 /** 221 * Test the sync grades task during several runs and for a series of grade changes. 222 * 223 * @covers ::execute 224 */ 225 public function test_grade_sync_chronological_syncs() { 226 $this->resetAfterTest(); 227 228 [$course, $resource] = $this->create_test_environment(); 229 $launchservice = $this->get_tool_launch_service(); 230 $task = $this->get_task_with_mocked_grade_service(); 231 232 // Launch the resource for an instructor which will create the domain objects needed for service calls. 233 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 234 $instructoruser = $this->getDataGenerator()->create_user(); 235 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 236 237 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 238 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 239 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 240 241 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 242 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); 243 $student1user = $this->getDataGenerator()->create_user(); 244 $student2user = $this->getDataGenerator()->create_user(); 245 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 246 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 247 248 // Grade student1 only. 249 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 250 251 // Sync and verify that only student1's grade is sent. 252 ob_start(); 253 $task->execute(); 254 $ob = ob_get_contents(); 255 ob_end_clean(); 256 $expectedtraces = [ 257 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 258 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 259 "'$course->id'.", 260 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 261 "'$resource->id' and the course '$course->id' was sent.", 262 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 263 "'$course->id'.", 264 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 265 "Processed 3 users; sent 1 grades." 266 ]; 267 foreach ($expectedtraces as $expectedtrace) { 268 $this->assertStringContainsString($expectedtrace, $ob); 269 } 270 271 // Sync again, verifying no grades are sent because nothing has changed. 272 ob_start(); 273 $task->execute(); 274 $ob = ob_get_contents(); 275 ob_end_clean(); 276 $expectedtraces = [ 277 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 278 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 279 "'$course->id'.", 280 "Not sent - The grade for the user '$student1id', for the resource '$resource->id' and the course ". 281 "'$course->id' was not sent as the grades are the same.", 282 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 283 "'$course->id'.", 284 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 285 "Processed 3 users; sent 0 grades." 286 ]; 287 foreach ($expectedtraces as $expectedtrace) { 288 $this->assertStringContainsString($expectedtrace, $ob); 289 } 290 291 // Change student1's grade and add a grade for student2. 292 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 68.5, $resource); 293 $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 44.5, $resource); 294 295 // Sync again, verifying both grade changes are sent. 296 ob_start(); 297 $task->execute(); 298 $ob = ob_get_contents(); 299 ob_end_clean(); 300 301 $expectedtraces = [ 302 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 303 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 304 "'$course->id'.", 305 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 306 "'$resource->id' and the course '$course->id' was sent.", 307 "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". 308 "'$resource->id' and the course '$course->id' was sent.", 309 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 310 "Processed 3 users; sent 2 grades." 311 ]; 312 foreach ($expectedtraces as $expectedtrace) { 313 $this->assertStringContainsString($expectedtrace, $ob); 314 } 315 } 316 317 /** 318 * Test a grade sync when there are more than one resource link for the resource. 319 * 320 * @covers ::execute 321 */ 322 public function test_grade_sync_multiple_resource_links() { 323 $this->resetAfterTest(); 324 325 [$course, $resource] = $this->create_test_environment(); 326 $launchservice = $this->get_tool_launch_service(); 327 $task = $this->get_task_with_mocked_grade_service(); 328 329 // Launch the resource first for an instructor using the default resource link in the platform. 330 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 331 $instructoruser = $this->getDataGenerator()->create_user(); 332 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 333 334 // Launch again as the instructor, this time from a different resource link in the platform. 335 $teachermocklaunch2 = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 'RLID-2'); 336 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch2); 337 338 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 339 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 340 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 341 342 $student1reslink1launch = $this->get_mock_launch($resource, $studentusers[0]); 343 $student2reslink1launch = $this->get_mock_launch($resource, $studentusers[1]); 344 $student1reslink2launch = $this->get_mock_launch($resource, $studentusers[1], 'RLID-2'); 345 $student1user = $this->getDataGenerator()->create_user(); 346 $student2user = $this->getDataGenerator()->create_user(); 347 [$student1id] = $launchservice->user_launches_tool($student1user, $student1reslink1launch); 348 [$student2id] = $launchservice->user_launches_tool($student2user, $student2reslink1launch); 349 $launchservice->user_launches_tool($student1user, $student1reslink2launch); 350 351 // Grade student1 only. 352 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 353 354 // Sync and verify that only student1's grade is sent but that it's sent for BOTH resource links. 355 ob_start(); 356 $task->execute(); 357 $ob = ob_get_contents(); 358 ob_end_clean(); 359 360 $expectedtraces = [ 361 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 362 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 363 "'$course->id'.", 364 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 365 "'$resource->id' and the course '$course->id' was sent.", 366 "Found 2 resource link(s) for the user '$student1id', for the resource ". 367 "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.", 368 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 369 "'$course->id'.", 370 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 371 "Processed 3 users; sent 1 grades." 372 ]; 373 foreach ($expectedtraces as $expectedtrace) { 374 $this->assertStringContainsString($expectedtrace, $ob); 375 } 376 377 // Verify that the grade was reported as being synced twice - once for each resource link. 378 $expected = "/Found 2 resource link\(s\) for the user '$student1id', for the resource ". 379 "'$resource->id' and the course '$course->id'. Attempting to sync grades for all.\n". 380 "Processing resource link '.*'.\n". 381 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 382 "'$resource->id' and the course '$course->id' was sent.\n". 383 "Processing resource link '.*'.\n". 384 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 385 "'$resource->id' and the course '$course->id' was sent./"; 386 $this->assertMatchesRegularExpression($expected, $ob); 387 } 388 389 /** 390 * Test grade sync when the resource has syncgrades disabled. 391 * 392 * @covers ::execute 393 */ 394 public function test_sync_grades_gradesync_disabled() { 395 $this->resetAfterTest(); 396 [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, 397 false); 398 $launchservice = $this->get_tool_launch_service(); 399 400 // Launch the resource for an instructor which will create the domain objects needed for service calls. 401 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 402 $instructoruser = $this->getDataGenerator()->create_user(); 403 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 404 405 $task = $this->get_task_with_mocked_grade_service(); 406 $this->expectOutputRegex('/Skipping task - There are no resources with grade sync enabled./'); 407 $task->execute(); 408 } 409 410 /** 411 * Test the grade sync task when the auth_lti plugin is disabled. 412 * 413 * @covers ::execute 414 */ 415 public function test_sync_grades_auth_plugin_disabled() { 416 $this->resetAfterTest(); 417 [$course, $resource] = $this->create_test_environment(false); 418 $launchservice = $this->get_tool_launch_service(); 419 420 // Launch the resource for an instructor which will create the domain objects needed for service calls. 421 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 422 $instructoruser = $this->getDataGenerator()->create_user(); 423 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 424 425 $task = $this->get_task_with_mocked_grade_service(); 426 $this->expectOutputRegex('/Skipping task - ' . 427 get_string('pluginnotenabled', 'auth', get_string('pluginname', 'auth_lti')) . '/'); 428 $task->execute(); 429 } 430 431 /** 432 * Test the grade sync task when the enrol_lti plugin is disabled. 433 * 434 * @covers ::execute 435 */ 436 public function test_sync_grades_enrol_plugin_disabled() { 437 $this->resetAfterTest(); 438 [$course, $resource] = $this->create_test_environment(true, false); 439 $launchservice = $this->get_tool_launch_service(); 440 441 // Launch the resource for an instructor which will create the domain objects needed for service calls. 442 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 443 $instructoruser = $this->getDataGenerator()->create_user(); 444 $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 445 446 $task = $this->get_task_with_mocked_grade_service(); 447 $this->expectOutputRegex('/Skipping task - ' . get_string('enrolisdisabled', 'enrol_lti') . '/'); 448 $task->execute(); 449 } 450 451 /** 452 * Test the grade sync task when the launch data doesn't include the AGS support. 453 * 454 * @covers ::execute 455 */ 456 public function test_sync_grades_no_service_endpoint() { 457 $this->resetAfterTest(); 458 [$course, $resource] = $this->create_test_environment(); 459 $launchservice = $this->get_tool_launch_service(); 460 461 // Launch the resource for an instructor which will create the domain objects needed for service calls. 462 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], 463 null, null); 464 $instructoruser = $this->getDataGenerator()->create_user(); 465 [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 466 467 $task = $this->get_task_with_mocked_grade_service(); 468 $this->expectOutputRegex( 469 "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". 470 "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". 471 "course '$course->id'. Attempting to sync grades for all.\n". 472 "Processing resource link '.*'.\n". 473 "Skipping - No grade service found for the user '$userid', for the resource '$resource->id' and the ". 474 "course '$course->id'.\n". 475 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". 476 "sent 0 grades./" 477 ); 478 $task->execute(); 479 } 480 481 /** 482 * Test syncing grades when the enrolment instance is disabled. 483 * 484 * @covers ::execute 485 */ 486 public function test_sync_grades_disabled_instance() { 487 $this->resetAfterTest(); 488 global $DB; 489 490 [$course, $resource, $resource2, $resource3] = $this->create_test_environment(); 491 492 // Disable resource 1. 493 $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED]; 494 $DB->update_record('enrol', $enrol); 495 496 // Delete the activity being shared by resource 2, leaving resource 2 disabled as a result. 497 $modcontext = \context::instance_by_id($resource2->contextid); 498 course_delete_module($modcontext->instanceid); 499 500 // Only the enabled resource 3 should sync grades. 501 $task = $this->get_task_with_mocked_grade_service(); 502 $this->expectOutputRegex( 503 "/^Starting - LTI Advantage grade sync for shared resource '$resource3->id' in course '$course->id'.\n". 504 "Completed - Synced grades for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ". 505 "sent 0 grades.\n$/" 506 ); 507 $task->execute(); 508 } 509 510 /** 511 * Test the grade sync when the context has been deleted in between launch and when the grade sync task is run. 512 * 513 * @covers ::execute 514 */ 515 public function test_sync_grades_deleted_context() { 516 $this->resetAfterTest(); 517 global $DB; 518 519 [$course, $resource] = $this->create_test_environment(); 520 $launchservice = $this->get_tool_launch_service(); 521 522 // Launch the resource for an instructor which will create the domain objects needed for service calls. 523 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 524 $instructoruser = $this->getDataGenerator()->create_user(); 525 [$userid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 526 527 // Delete the activity, then enable the enrolment method (it is disabled during activity deletion). 528 $modcontext = \context::instance_by_id($resource->contextid); 529 course_delete_module($modcontext->instanceid); 530 $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_ENABLED]; 531 $DB->update_record('enrol', $enrol); 532 533 $task = $this->get_task_with_mocked_grade_service(); 534 $this->expectOutputRegex( 535 "/Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.\n". 536 "Found 1 resource link\(s\) for the user '$userid', for the resource '$resource->id' and the ". 537 "course '$course->id'. Attempting to sync grades for all.\n". 538 "Processing resource link '.*'.\n". 539 "Failed - Invalid contextid '$resource->contextid' for the resource '$resource->id'.\n". 540 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. Processed 1 users; ". 541 "sent 0 grades./" 542 ); 543 $task->execute(); 544 } 545 546 /** 547 * Test grade sync when completion is required for the activity before sync takes place. 548 * 549 * @covers ::execute 550 */ 551 public function test_sync_grades_completion_required() { 552 $this->resetAfterTest(); 553 global $CFG; 554 require_once($CFG->libdir . '/completionlib.php'); 555 556 [ 557 $course, 558 $resource, 559 $resource2, 560 $publishedcourse 561 ] = $this->create_test_environment(true, true, false, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, true); 562 $launchservice = $this->get_tool_launch_service(); 563 $task = $this->get_task_with_mocked_grade_service(); 564 565 // Launch the resource for an instructor which will create the domain objects needed for service calls. 566 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 567 $instructoruser = $this->getDataGenerator()->create_user(); 568 [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 569 570 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 571 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 572 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 573 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 574 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1]); 575 $student1user = $this->getDataGenerator()->create_user(); 576 $student2user = $this->getDataGenerator()->create_user(); 577 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 578 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 579 580 // Launch the published course as student2. 581 $student2mockcourselaunch = $this->get_mock_launch($publishedcourse, $studentusers[1], '23456'); 582 $launchservice->user_launches_tool($student2user, $student2mockcourselaunch); 583 584 // Grade student1 in the assign resource. 585 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 586 587 // And student2 in the course resource. 588 $expectedstudent2grade = $this->set_user_grade_for_resource($student2id, 55.5, $publishedcourse); 589 590 // Sync and verify that no grades are sent because resource and published course are both not yet complete. 591 ob_start(); 592 $task->execute(); 593 $ob = ob_get_contents(); 594 ob_end_clean(); 595 $expectedtraces = [ 596 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 597 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 598 "'$course->id'.", 599 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 600 "'$course->id'.", 601 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 602 "'$course->id'.", 603 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 604 "the course '$course->id'.", 605 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 606 "Processed 3 users; sent 0 grades." 607 ]; 608 foreach ($expectedtraces as $expectedtrace) { 609 $this->assertStringContainsString($expectedtrace, $ob); 610 } 611 612 // Complete the resource for student1. 613 $this->override_resource_completion_status_for_user($resource, $student1id, true); 614 615 // Run the sync again, this time confirming the grade for student1 is sent. 616 ob_start(); 617 $task->execute(); 618 $ob = ob_get_contents(); 619 ob_end_clean(); 620 $expectedtraces = [ 621 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 622 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 623 "'$course->id'.", 624 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 625 "'$resource->id' and the course '$course->id' was sent.", 626 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 627 "'$course->id'.", 628 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 629 "Processed 3 users; sent 1 grades.", 630 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 631 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 632 "the course '$course->id'.", 633 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 634 "Processed 1 users; sent 0 grades.", 635 ]; 636 foreach ($expectedtraces as $expectedtrace) { 637 $this->assertStringContainsString($expectedtrace, $ob); 638 } 639 640 // Fail completion for student1 and confirm no grade is sent, even despite it being changed. 641 $this->set_user_grade_for_resource($student1id, 33.3, $resource); 642 $this->override_resource_completion_status_for_user($resource, $student1id, false); 643 644 ob_start(); 645 $task->execute(); 646 $ob = ob_get_contents(); 647 ob_end_clean(); 648 $expectedtraces = [ 649 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 650 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 651 "'$course->id'.", 652 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 653 "'$course->id'.", 654 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 655 "'$course->id'.", 656 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 657 "Processed 3 users; sent 0 grades.", 658 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 659 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 660 "the course '$course->id'.", 661 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 662 "Processed 1 users; sent 0 grades.", 663 ]; 664 foreach ($expectedtraces as $expectedtrace) { 665 $this->assertStringContainsString($expectedtrace, $ob); 666 } 667 668 // Complete the course for student2 and verify the grade is now sent. 669 $this->override_resource_completion_status_for_user($publishedcourse, $student2id, true); 670 671 ob_start(); 672 $task->execute(); 673 $ob = ob_get_contents(); 674 ob_end_clean(); 675 $expectedtraces = [ 676 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 677 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 678 "'$course->id'.", 679 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 680 "'$course->id'.", 681 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 682 "'$course->id'.", 683 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 684 "Processed 3 users; sent 0 grades.", 685 "Starting - LTI Advantage grade sync for shared resource '$publishedcourse->id' in course '$course->id'.", 686 "Success - The grade '$expectedstudent2grade' for the user '$student2id', for the resource ". 687 "'$publishedcourse->id' and the course '$course->id' was sent.", 688 "Completed - Synced grades for tool '$publishedcourse->id' in the course '$course->id'. ". 689 "Processed 1 users; sent 1 grades.", 690 691 ]; 692 foreach ($expectedtraces as $expectedtrace) { 693 $this->assertStringContainsString($expectedtrace, $ob); 694 } 695 696 // Mark the course as in progress again for student2 and verify any new grade changes are not sent. 697 $this->set_user_grade_for_resource($student2id, 78.8, $publishedcourse); 698 $this->override_resource_completion_status_for_user($publishedcourse, $student2id, false); 699 700 ob_start(); 701 $task->execute(); 702 $ob = ob_get_contents(); 703 ob_end_clean(); 704 $expectedtraces = [ 705 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 706 "Activity not completed for the user '$teacherid', for the resource '$resource->id' and the course ". 707 "'$course->id'.", 708 "Activity not completed for the user '$student1id', for the resource '$resource->id' and the course ". 709 "'$course->id'.", 710 "Activity not completed for the user '$student2id', for the resource '$resource->id' and the course ". 711 "'$course->id'.", 712 "Skipping - Course not completed for the user '$student2id', for the resource '$publishedcourse->id' and ". 713 "the course '$course->id'.", 714 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 715 "Processed 3 users; sent 0 grades." 716 ]; 717 foreach ($expectedtraces as $expectedtrace) { 718 $this->assertStringContainsString($expectedtrace, $ob); 719 } 720 } 721 722 /** 723 * Test grade sync when the attempt to call the service returns an exception or a bad HTTP response code. 724 * 725 * @covers ::execute 726 */ 727 public function test_sync_grades_failed_service_call() { 728 $this->resetAfterTest(); 729 [$course, $resource] = $this->create_test_environment(); 730 $launchservice = $this->get_tool_launch_service(); 731 $task = $this->get_task_with_mocked_grade_service('200', true); 732 733 // Launch the resource for an instructor which will create the domain objects needed for service calls. 734 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0]); 735 $instructoruser = $this->getDataGenerator()->create_user(); 736 [$teacherid] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 737 738 // Launch the resource for a student, creating the enrolment and allowing grading to take place. 739 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 740 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 741 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0]); 742 $student1user = $this->getDataGenerator()->create_user(); 743 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 744 745 // Grade student1 in the assign resource. 746 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 747 748 // Run the sync, verifying that the response error causes a 'Failed' trace but that the task completes. 749 ob_start(); 750 $task->execute(); 751 $ob = ob_get_contents(); 752 ob_end_clean(); 753 $expectedtraces = [ 754 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 755 "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 756 "'$resource->id' and the course '$course->id' failed to send.", 757 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 758 "Processed 2 users; sent 0 grades." 759 ]; 760 foreach ($expectedtraces as $expectedtrace) { 761 $this->assertStringContainsString($expectedtrace, $ob); 762 } 763 764 // Now run the sync again, this time with a bad http response code. 765 $task = $this->get_task_with_mocked_grade_service('400'); 766 ob_start(); 767 $task->execute(); 768 $ob = ob_get_contents(); 769 ob_end_clean(); 770 $expectedtraces = [ 771 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 772 "Failed - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 773 "'$resource->id' and the course '$course->id' failed to send.", 774 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 775 "Processed 2 users; sent 0 grades." 776 ]; 777 foreach ($expectedtraces as $expectedtrace) { 778 $this->assertStringContainsString($expectedtrace, $ob); 779 } 780 } 781 782 /** 783 * Test the sync when only the lineitem URL is provided and when lineitem creation/query isn't expected. 784 * 785 * @covers ::execute 786 */ 787 public function test_sync_grades_coupled_lineitem() { 788 $this->resetAfterTest(); 789 790 [$course, $resource] = $this->create_test_environment(); 791 $launchservice = $this->get_tool_launch_service(); 792 793 // The launches use a coupled line item. Only scores can be posted. Line items and results cannot be created or queried. 794 $agsclaim = [ 795 "scope" => ["https://purl.imsglobal.org/spec/lti-ags/scope/score"], 796 "lineitem" => "https://platform.example.com/10/lineitems/45/lineitem" 797 ]; 798 799 // Launch the resource for an instructor which will create the domain objects needed for service calls. 800 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, 801 $agsclaim); 802 $instructoruser = $this->getDataGenerator()->create_user(); 803 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 804 805 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 806 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 807 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 808 809 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); 810 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); 811 $student1user = $this->getDataGenerator()->create_user(); 812 $student2user = $this->getDataGenerator()->create_user(); 813 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 814 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 815 816 // Grade student1 only. 817 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 818 819 // Mock task, asserting that score posting to an existing line item takes place, via a mock grade service object. 820 $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); 821 $mockgradeservice->method('putGrade')->willReturnCallback(function() { 822 return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; 823 }); 824 $mockgradeservice->expects($this->never()) 825 ->method('findOrCreateLineitem'); 826 $mockgradeservice->expects($this->once()) 827 ->method('putGrade') 828 ->with($this->isInstanceOf(LtiGrade::class)); 829 $mocktask = $this->getMockBuilder(sync_grades::class) 830 ->onlyMethods(['get_ags']) 831 ->getMock(); 832 $mocktask->method('get_ags')->willReturn($mockgradeservice); 833 834 // Sync and verify that only student1's grade is sent. 835 ob_start(); 836 $mocktask->execute(); 837 $ob = ob_get_contents(); 838 ob_end_clean(); 839 $expectedtraces = [ 840 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 841 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 842 "'$course->id'.", 843 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 844 "'$resource->id' and the course '$course->id' was sent.", 845 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 846 "'$course->id'.", 847 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 848 "Processed 3 users; sent 1 grades." 849 ]; 850 foreach ($expectedtraces as $expectedtrace) { 851 $this->assertStringContainsString($expectedtrace, $ob); 852 } 853 } 854 855 /** 856 * Test the sync for an activity context when only the lineitems URL is provided and when line item creation/query is expected. 857 * 858 * @covers ::execute 859 */ 860 public function test_sync_grades_none_or_many_lineitems_activity_context() { 861 $this->resetAfterTest(); 862 863 [$course, $resource] = $this->create_test_environment(); 864 $launchservice = $this->get_tool_launch_service(); 865 866 // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items. 867 $agsclaim = [ 868 "scope" => [ 869 "https://purl.imsglobal.org/spec/lti-ags/scope/score", 870 "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", 871 ], 872 "lineitems" => "https://platform.example.com/10/lineitems" 873 ]; 874 875 // Launch the resource for an instructor which will create the domain objects needed for service calls. 876 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, 877 $agsclaim); 878 $instructoruser = $this->getDataGenerator()->create_user(); 879 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 880 881 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 882 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 883 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 884 885 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); 886 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); 887 $student1user = $this->getDataGenerator()->create_user(); 888 $student2user = $this->getDataGenerator()->create_user(); 889 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 890 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 891 892 // Grade student1 only. 893 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 894 895 // Mock task, asserting that line item creation takes place via a mock grade service object. 896 $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); 897 $mockgradeservice->method('putGrade')->willReturnCallback(function() { 898 return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; 899 }); 900 $mockgradeservice->expects($this->once()) 901 ->method('findOrCreateLineitem'); 902 $mockgradeservice->expects($this->once()) 903 ->method('putGrade') 904 ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class)); 905 $mocktask = $this->getMockBuilder(sync_grades::class) 906 ->onlyMethods(['get_ags']) 907 ->getMock(); 908 $mocktask->method('get_ags')->willReturn($mockgradeservice); 909 910 // Sync and verify that only student1's grade is sent. 911 ob_start(); 912 $mocktask->execute(); 913 $ob = ob_get_contents(); 914 ob_end_clean(); 915 $expectedtraces = [ 916 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 917 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 918 "'$course->id'.", 919 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 920 "'$resource->id' and the course '$course->id' was sent.", 921 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 922 "'$course->id'.", 923 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 924 "Processed 3 users; sent 1 grades." 925 ]; 926 foreach ($expectedtraces as $expectedtrace) { 927 $this->assertStringContainsString($expectedtrace, $ob); 928 } 929 } 930 931 /** 932 * Test the sync for a course context when only the lineitems URL is provided and when line item creation/query is expected. 933 * 934 * @covers ::execute 935 */ 936 public function test_sync_grades_none_or_many_lineitems_course_context() { 937 $this->resetAfterTest(); 938 939 [$course, $tool1, $tool2, $resource] = $this->create_test_environment(); 940 $launchservice = $this->get_tool_launch_service(); 941 942 // The launches omit the 'lineitem' claim, meaning the item may have none (or many) line items. 943 $agsclaim = [ 944 "scope" => [ 945 "https://purl.imsglobal.org/spec/lti-ags/scope/score", 946 "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", 947 ], 948 "lineitems" => "https://platform.example.com/10/lineitems" 949 ]; 950 951 // Launch the resource for an instructor which will create the domain objects needed for service calls. 952 $teachermocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'], false)[0], null, 953 $agsclaim); 954 $instructoruser = $this->getDataGenerator()->create_user(); 955 [$teacherid, $resource] = $launchservice->user_launches_tool($instructoruser, $teachermocklaunch); 956 957 // Launch the resource for a few more users, creating those enrolments and allowing grading to take place. 958 $studentusers = $this->get_mock_launch_users_with_ids(['2', '3'], false, 959 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'); 960 961 $student1mocklaunch = $this->get_mock_launch($resource, $studentusers[0], null, $agsclaim); 962 $student2mocklaunch = $this->get_mock_launch($resource, $studentusers[1], null, $agsclaim); 963 $student1user = $this->getDataGenerator()->create_user(); 964 $student2user = $this->getDataGenerator()->create_user(); 965 [$student1id] = $launchservice->user_launches_tool($student1user, $student1mocklaunch); 966 [$student2id] = $launchservice->user_launches_tool($student2user, $student2mocklaunch); 967 968 // Grade student1 only. 969 $expectedstudent1grade = $this->set_user_grade_for_resource($student1id, 65, $resource); 970 971 // Mock task, asserting that line item creation takes place via a mock grade service object. 972 $mockgradeservice = $this->createMock(LtiAssignmentsGradesService::class); 973 $mockgradeservice->method('putGrade')->willReturnCallback(function() { 974 return ['headers' => ['httpstatus' => "HTTP/2 200 OK"], 'body' => '', 'status' => 200]; 975 }); 976 $mockgradeservice->expects($this->once()) 977 ->method('findOrCreateLineitem'); 978 $mockgradeservice->expects($this->once()) 979 ->method('putGrade') 980 ->with($this->isInstanceOf(LtiGrade::class), $this->isInstanceOf(LtiLineitem::class)); 981 $mocktask = $this->getMockBuilder(sync_grades::class) 982 ->onlyMethods(['get_ags']) 983 ->getMock(); 984 $mocktask->method('get_ags')->willReturn($mockgradeservice); 985 986 // Sync and verify that only student1's grade is sent. 987 ob_start(); 988 $mocktask->execute(); 989 $ob = ob_get_contents(); 990 ob_end_clean(); 991 $expectedtraces = [ 992 "Starting - LTI Advantage grade sync for shared resource '$resource->id' in course '$course->id'.", 993 "Skipping - Invalid grade for the user '$teacherid', for the resource '$resource->id' and the course ". 994 "'$course->id'.", 995 "Success - The grade '$expectedstudent1grade' for the user '$student1id', for the resource ". 996 "'$resource->id' and the course '$course->id' was sent.", 997 "Skipping - Invalid grade for the user '$student2id', for the resource '$resource->id' and the course ". 998 "'$course->id'.", 999 "Completed - Synced grades for tool '$resource->id' in the course '$course->id'. ". 1000 "Processed 3 users; sent 1 grades." 1001 ]; 1002 foreach ($expectedtraces as $expectedtrace) { 1003 $this->assertStringContainsString($expectedtrace, $ob); 1004 } 1005 } 1006 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body