Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace enrol_lti\local\ltiadvantage\repository;
use enrol_lti\local\ltiadvantage\entity\application_registration;
use enrol_lti\local\ltiadvantage\entity\user;
/**
* Tests for user_repository objects.
*
* @package enrol_lti
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\user_repository
*/
class user_repository_test extends \advanced_testcase {
/**
* Helper to generate a new user instance.
*
* @param int $mockresourceid used to spoof a published resource, to which this user is associated.
* @param array $userfields user information like city, timezone which would normally come from the tool configuration.
* @return user a user instance
*/
protected function generate_user(int $mockresourceid = 1, array $userfields = []): user {
global $CFG;
$registration = application_registration::create(
'Test',
'a2c94a2c94',
new \moodle_url('http://lms.example.org'),
'clientid_123',
new \moodle_url('https://example.org/authrequesturl'),
new \moodle_url('https://example.org/jwksurl'),
new \moodle_url('https://example.org/accesstokenurl')
);
$registrationrepo = new application_registration_repository();
$createdregistration = $registrationrepo->save($registration);
$deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123');
$deploymentrepo = new deployment_repository();
$saveddeployment = $deploymentrepo->save($deployment);
$contextrepo = new context_repository();
$context = $saveddeployment->add_context(
'CTX123',
['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection']
);
$savedcontext = $contextrepo->save($context);
$resourcelinkrepo = new resource_link_repository();
$resourcelink = $saveddeployment->add_resource_link('resourcelinkid_123', $mockresourceid,
$savedcontext->get_id());
$savedresourcelink = $resourcelinkrepo->save($resourcelink);
// Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth.
$user = $this->getDataGenerator()->create_user([
'city' => '',
'country' => '',
'institution' => '',
'timezone' => '99',
'maildisplay' => 2,
'lang' => 'en'
]);
$userdefaultvalues = [
'lang' => $CFG->lang,
'city' => '',
'country' => '',
'institution' => '',
'timezone' => '99',
'maildisplay' => 2
];
if (empty($userfields)) {
// If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged).
$userfields = $userdefaultvalues;
} else {
// If they have been provided, merge and override the defaults.
$userfields = array_merge($userdefaultvalues, $userfields);
}
$ltiuser = $savedresourcelink->add_user(
$user->id,
'source-id-123',
...array_values($userfields)
);
$ltiuser->set_lastgrade(67.33333333);
return $ltiuser;
}
/**
* Helper to assert that all the key elements of two users (i.e. excluding id) are equal.
*
* @param user $expected the user whose values are deemed correct.
* @param user $check the user to check.
* @param bool $checkresourcelink whether or not to confirm the resource link value matches too.
*/
protected function assert_same_user_values(user $expected, user $check, bool $checkresourcelink = false): void {
$this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid());
$this->assertEquals($expected->get_city(), $check->get_city());
$this->assertEquals($expected->get_country(), $check->get_country());
$this->assertEquals($expected->get_institution(), $check->get_institution());
$this->assertEquals($expected->get_timezone(), $check->get_timezone());
$this->assertEquals($expected->get_maildisplay(), $check->get_maildisplay());
$this->assertEquals($expected->get_lang(), $check->get_lang());
if ($checkresourcelink) {
$this->assertEquals($expected->get_resourcelinkid(), $check->get_resourcelinkid());
}
}
/**
* Helper to assert that all the key elements of a user are present in the DB.
*
* @param user $expected the user whose values are deemed correct.
*/
protected function assert_user_db_values(user $expected) {
global $DB;
$sql = "SELECT u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone,
u.maildisplay, u.mnethostid, u.confirmed, u.lang, u.auth
FROM {enrol_lti_users} lu
JOIN {user} u
ON (lu.userid = u.id)
WHERE lu.id = :id";
$userrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]);
$this->assertEquals($expected->get_city(), $userrecord->city);
$this->assertEquals($expected->get_country(), $userrecord->country);
$this->assertEquals($expected->get_institution(), $userrecord->institution);
$this->assertEquals($expected->get_timezone(), $userrecord->timezone);
$this->assertEquals($expected->get_maildisplay(), $userrecord->maildisplay);
$this->assertEquals($expected->get_lang(), $userrecord->lang);
$ltiuserrecord = $DB->get_record('enrol_lti_users', ['id' => $expected->get_id()]);
$this->assertEquals($expected->get_id(), $ltiuserrecord->id);
$this->assertEquals($expected->get_sourceid(), $ltiuserrecord->sourceid);
$this->assertEquals($expected->get_resourceid(), $ltiuserrecord->toolid);
$this->assertEquals($expected->get_lastgrade(), $ltiuserrecord->lastgrade);
if ($expected->get_resourcelinkid()) {
$sql = "SELECT rl.id
FROM {enrol_lti_users} lu
JOIN {enrol_lti_user_resource_link} rlj
ON (lu.id = rlj.ltiuserid)
JOIN {enrol_lti_resource_link} rl
ON (rl.id = rlj.resourcelinkid)
WHERE lu.id = :id";
$resourcelinkrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]);
$this->assertEquals($expected->get_resourcelinkid(), $resourcelinkrecord->id);
}
}
/**
* Tests adding a user to the store, assuming that the user has been created using the default 'user default values'.
*
* @covers ::save
*/
public function test_save_new_unchanged_user_defaults(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertIsInt($saveduser->get_id());
$this->assert_same_user_values($user, $saveduser, true);
$this->assert_user_db_values($saveduser);
// No change to underlying user: city, etc. take on default values matching those of the existing user record.
$this->assertEmpty($events);
}
/**
* Tests adding a user to the store, assuming that the user has been created using modified 'user default values'.
*
* @covers ::save
*/
public function test_save_new_changed_user_defaults(): void {
$this->resetAfterTest();
$user = $this->generate_user(1, ['city' => 'Perth']);
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertIsInt($saveduser->get_id());
$this->assert_same_user_values($user, $saveduser, true);
$this->assert_user_db_values($saveduser);
// The underlying user record will change: city ('Perth') differs from that of the existing user ('').
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
}
/**
* Test saving an existing user instance.
*
* @covers ::save
*/
public function test_save_existing(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$sink = $this->redirectEvents();
$saveduser = $userrepo->save($user);
$events = $sink->get_events();
$sink->close();
$this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged.
$saveduser->set_city('New City');
$saveduser->set_country('NZ');
$saveduser->set_lastgrade(99.99999999);
$sink = $this->redirectEvents();
$saveduser2 = $userrepo->save($saveduser);
$events = $sink->get_events();
$sink->close();
$this->assertEquals($saveduser->get_id(), $saveduser2->get_id());
$this->assert_same_user_values($saveduser, $saveduser2, true);
$this->assert_user_db_values($saveduser2);
// The underlying user record will change now, since city and country have changed.
$this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
}
/**
* Test saving an instance which exists by id, but has a different localid to the data in the store.
*
* @covers ::save
*/
public function test_save_existing_localid_mismatch(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$user2 = user::create(
$saveduser->get_resourceid(),
999999,
$saveduser->get_deploymentid(),
$saveduser->get_sourceid(),
$saveduser->get_lang(),
$saveduser->get_timezone(),
'',
'',
'',
null,
null,
null,
null,
$saveduser->get_id()
);
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage("Cannot update user mapping. LTI user '{$saveduser->get_id()}' is already mapped " .
"to user '{$saveduser->get_localid()}' and can't be associated with another user '999999'.");
$userrepo->save($user2);
}
/**
* Test trying to save a user with an id that is invalid.
*
* @covers ::save
*/
public function test_save_stale_id(): void {
global $CFG;
$this->resetAfterTest();
$instructoruser = $this->getDataGenerator()->create_user();
$userrepo = new user_repository();
$user = user::create(
4,
$instructoruser->id,
5,
'source-id-123',
$CFG->lang,
'99',
'',
'',
'',
null,
null,
null,
null,
999999
);
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage("Cannot save lti user with id '999999'. The record does not exist.");
$userrepo->save($user);
}
/**
* Verify that trying to save a stale object results in an exception referring to unique constraint violation.
*
* @covers ::save
*/
public function test_save_uniqueness_constraint(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$userrepo->save($user);
$this->expectException(\coding_exception::class);
$this->expectExceptionMessageMatches("/Cannot create duplicate LTI user '[a-z0-9_]*' for resource '[0-9]*'/");
$userrepo->save($user);
}
/**
* Test finding a user instance by id.
*
* @covers ::find
*/
public function test_find(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$founduser = $userrepo->find($saveduser->get_id());
$this->assertIsInt($founduser->get_id());
$this->assert_same_user_values($saveduser, $founduser, false);
$this->assertNull($userrepo->find(0));
}
/**
* Test finding all of users associated with a given published resource.
*
* @covers ::find_by_resource
*/
public function test_find_by_resource(): void {
global $CFG;
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$instructoruser = $this->getDataGenerator()->create_user();
$user2 = user::create(
$saveduser->get_resourceid(),
$instructoruser->id,
$saveduser->get_deploymentid(),
'another-user-123',
$CFG->lang,
'99',
'Perth',
'AU',
'An Example Institution',
2
);
$saveduser2 = $userrepo->save($user2);
$savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2];
$foundusers = $userrepo->find_by_resource($saveduser->get_resourceid());
$this->assertCount(2, $foundusers);
foreach ($foundusers as $founduser) {
$this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser);
}
}
/**
* Test that users can be found based on their resource_link association.
*
* @covers ::find_by_resource_link
*/
public function test_find_by_resource_link(): void {
global $CFG;
$this->resetAfterTest();
$user = $this->generate_user();
$user->set_resourcelinkid(33);
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$instructoruser = $this->getDataGenerator()->create_user();
$user2 = user::create(
$saveduser->get_resourceid(),
$instructoruser->id,
$saveduser->get_deploymentid(),
'another-user-123',
$CFG->lang,
'99',
'Perth',
'AU',
'An Example Institution',
2,
null,
null,
33
);
$saveduser2 = $userrepo->save($user2);
$savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2];
$foundusers = $userrepo->find_by_resource_link(33);
$this->assertCount(2, $foundusers);
foreach ($foundusers as $founduser) {
$this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser);
}
}
/**
* Test checking existence of a user instance, based on id.
*
* @covers ::exists
*/
public function test_exists(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$this->assertTrue($userrepo->exists($saveduser->get_id()));
$this->assertFalse($userrepo->exists(-50));
}
/**
* Test deleting a user instance, based on id.
*
* @covers ::delete
*/
public function test_delete(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$this->assertTrue($userrepo->exists($saveduser->get_id()));
$userrepo->delete($saveduser->get_id());
$this->assertFalse($userrepo->exists($saveduser->get_id()));
global $DB;
$this->assertFalse($DB->record_exists('enrol_lti_users', ['id' => $saveduser->get_id()]));
$this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['ltiuserid' => $saveduser->get_id()]));
$this->assertTrue($DB->record_exists('user', ['id' => $saveduser->get_localid()]));
$this->assertNull($userrepo->delete($saveduser->get_id()));
}
/**
* Test deleting a collection of lti user instances by deployment.
*
* @covers ::delete_by_deployment
*/
public function test_delete_by_deployment(): void {
global $CFG;
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$instructoruser = $this->getDataGenerator()->create_user();
$instructor2user = $this->getDataGenerator()->create_user();
$user2 = user::create(
$saveduser->get_resourceid(),
$instructoruser->id,
$saveduser->get_deploymentid(),
'another-user-123',
$CFG->lang,
'99',
'Perth',
'AU',
'An Example Institution',
);
$saveduser2 = $userrepo->save($user2);
$user3 = user::create(
$saveduser->get_resourceid(),
$instructor2user->id,
$saveduser->get_deploymentid() + 1,
'another-user-678',
$CFG->lang,
'99',
'Melbourne',
'AU',
'An Example Institution',
);
$saveduser3 = $userrepo->save($user3);
$this->assertTrue($userrepo->exists($saveduser->get_id()));
$this->assertTrue($userrepo->exists($saveduser2->get_id()));
$this->assertTrue($userrepo->exists($saveduser3->get_id()));
$userrepo->delete_by_deployment($saveduser->get_deploymentid());
$this->assertFalse($userrepo->exists($saveduser->get_id()));
$this->assertFalse($userrepo->exists($saveduser2->get_id()));
$this->assertTrue($userrepo->exists($saveduser3->get_id()));
}
/**
* Verify a user who has been deleted can be re-saved to the repository and matched to an existing local user.
*
* @covers ::save
*/
public function test_save_deleted(): void {
$this->resetAfterTest();
$user = $this->generate_user();
$userrepo = new user_repository();
$saveduser = $userrepo->save($user);
$userrepo->delete($saveduser->get_id());
$this->assertFalse($userrepo->exists($saveduser->get_id()));
$saveduser2 = $userrepo->save($user);
$this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid());
$this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id());
}
/**
* Test confirming that any associated legacy lti user records are not returned by the repository.
*
* This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users.
*
* @covers ::find
* @covers ::find_single_user_by_resource
* @covers ::find_by_resource
*/
public function test_find_filters_legacy_lti_users(): void {
$this->resetAfterTest();
global $DB;
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]);
$ltiuserdata = [
'userid' => $user->id,
'toolid' => $resource->id,
'sourceid' => '1001',
];
$ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata);
$userrepo = new user_repository();
$this->assertNull($userrepo->find($ltiuserid));
$this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id));
$this->assertEmpty($userrepo->find_by_resource($resource->id));
// Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned.
$ltiuserdata['id'] = $ltiuserid;
$ltiuserdata['ltideploymentid'] = '234';
$DB->update_record('enrol_lti_users', $ltiuserdata);
$this->assertInstanceOf(user::class, $userrepo->find($ltiuserid));
$this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id));
$ltiusers = $userrepo->find_by_resource($resource->id);
$this->assertCount(1, $ltiusers);
$this->assertInstanceOf(user::class, reset($ltiusers));
}
}