Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }