MDL-75947 ltiservice_gradebookservices: test sequential score posting

This tests highlights a problem with the score handling logic and
will currently fail. On score post, the current time is used for
grade->timemodified instead of the score->timestamp provided by the
tool in the POST JSON.
This commit is contained in:
Jake Dallimore
2025-06-16 14:38:24 +09:30
committed by Jayce Birrell
parent c42efe971f
commit 3ecf81fcf4
@@ -17,6 +17,8 @@
namespace ltiservice_gradebookservices;
use ltiservice_gradebookservices\local\resources\lineitem;
use ltiservice_gradebookservices\local\resources\results;
use ltiservice_gradebookservices\local\resources\scores;
use ltiservice_gradebookservices\local\service\gradebookservices;
/**
@@ -174,6 +176,97 @@ final class lineitem_test extends \advanced_testcase {
$this->assertFalse(isset($responseitem->submissionReview->custom));
}
/**
* Test running a series of score updates, highlighting problems with the score posting logic.
*
* @covers ::execute
*
* @return void
*/
public function test_sequential_score_posts(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
$this->resetAfterTest();
$resourceid = 'test-resource-id';
$tag = 'tag';
$course = $this->getDataGenerator()->create_course();
$typeid = $this->create_type();
$user = $this->getDataGenerator()->create_and_enrol($course);
// Create mod instance with line item - nothing pushed via services yet.
$gbservice = new gradebookservices();
$gbservice->set_type(lti_get_type($typeid));
$modinstance = $this->create_graded_lti($typeid, $course, $resourceid, $tag);
$gradeitems = $gbservice->get_lineitems($course->id, null, null, null, null, null, $typeid);
$this->assertEquals(1, $gradeitems[0]); // The 1st item in the array is the items count.
// Post a score so that there's at least one grade present at the time of lineitem update.
$score = new scores($gbservice);
$_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST;
$_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid";
$token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']);
$_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token;
$_GET['type_id'] = (string)$typeid;
$score->scoreGiven = "8.0";
$score->scoreMaximum = "10.0";
$score->activityProgress = "Completed";
$score->timestamp = "2024-08-07T18:54:36.736+00:00";
$score->gradingProgress = "FullyGraded";
$score->userId = $user->id;
$response = new \mod_lti\local\ltiservice\response();
$response->set_content_type('application/vnd.ims.lis.v1.score+json');
$response->set_request_data(json_encode($score));
$score->execute($response);
// The grade in Moodle should reflect the score->timestamp, not the time of the score post.
$grades = grade_get_grades($course->id, 'mod', 'lti', $modinstance->id, $user->id);
$studentgrade = array_shift($grades->items[0]->grades);
$this->assertEquals(strtotime($score->timestamp), $studentgrade->dategraded);
// Read the results via the service. This should also return the correct dategraded time (timemodified in LTI terms).
$_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_GET;
$_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/results?type_id=$typeid";
$token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly']);
$_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token;
$_GET['type_id'] = (string)$typeid;
$result = new results($gbservice);
$response = new \mod_lti\local\ltiservice\response();
$response->set_content_type('application/vnd.ims.lis.v2.resultcontainer+json');
$result->execute($response);
$body = json_decode($response->get_body());
$result = array_shift($body);
$this->assertEquals(strtotime($score->timestamp), strtotime($result->timestamp));
// Now, try to post a newer score using a timestamp that is greater than the one originally sent, but less than the time at
// which the score was last posted. This should be valid since the timestamp is greater than the original posted score.
$_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST;
$_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid";
$token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']);
$_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token;
$_GET['type_id'] = (string)$typeid;
$score->scoreGiven = "14";
$score->timestamp = "2024-08-08T18:54:36.736+00:00";
$response = new \mod_lti\local\ltiservice\response();
$response->set_content_type('application/vnd.ims.lis.v1.score+json');
$response->set_request_data(json_encode($score));
$score->execute($response);
$this->assertEquals(200, json_decode($response->get_code()));
// Finally, post a score that's just been updated (i.e. score->timestamp = now).
$_SERVER['REQUEST_METHOD'] = \mod_lti\local\ltiservice\resource_base::HTTP_POST;
$_SERVER['PATH_INFO'] = "/$course->id/lineitems/{$gradeitems[1][0]->id}/lineitem/scores?type_id=$typeid";
$token = lti_new_access_token($typeid, ['https://purl.imsglobal.org/spec/lti-ags/scope/score']);
$_SERVER['HTTP_Authorization'] = 'Bearer '.$token->token;
$_GET['type_id'] = (string)$typeid;
$score->scoreGiven = "15";
$score->timestamp = date('c', time());
$response = new \mod_lti\local\ltiservice\response();
$response->set_content_type('application/vnd.ims.lis.v1.score+json');
$response->set_request_data(json_encode($score));
$score->execute($response);
$this->assertEquals(200, json_decode($response->get_code()));
}
/**
* Inserts a graded lti instance, which should create a grade_item and gradebookservices record.
*