From 7a1f43304331056202ae75a35fb3646c36156a10 Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Mon, 24 Jan 2022 16:22:51 +0800 Subject: [PATCH] MDL-69542 enrol_lti: add new entity classes for storing launch data --- .../local/ltiadvantage/entity/ags_info.php | 178 +++++ .../entity/application_registration.php | 186 +++++ .../local/ltiadvantage/entity/context.php | 179 +++++ .../local/ltiadvantage/entity/deployment.php | 178 +++++ .../ltiadvantage/entity/migration_claim.php | 191 +++++ .../local/ltiadvantage/entity/nrps_info.php | 131 ++++ .../ltiadvantage/entity/registration_url.php | 69 ++ .../ltiadvantage/entity/resource_link.php | 231 ++++++ .../local/ltiadvantage/entity/user.php | 420 +++++++++++ .../ltiadvantage/entity/ags_info_test.php | 276 +++++++ .../entity/application_registration_test.php | 241 ++++++ .../ltiadvantage/entity/context_test.php | 206 +++++ .../ltiadvantage/entity/deployment_test.php | 190 +++++ .../entity/migration_claim_test.php | 193 +++++ .../ltiadvantage/entity/nrps_info_test.php | 101 +++ .../entity/registration_url_test.php | 101 +++ .../entity/resource_link_test.php | 199 +++++ .../local/ltiadvantage/entity/user_test.php | 707 ++++++++++++++++++ 18 files changed, 3977 insertions(+) create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/ags_info.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/application_registration.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/context.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/deployment.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/migration_claim.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/nrps_info.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/registration_url.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/resource_link.php create mode 100644 enrol/lti/classes/local/ltiadvantage/entity/user.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/ags_info_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/application_registration_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/context_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/deployment_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/migration_claim_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/nrps_info_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/registration_url_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/resource_link_test.php create mode 100644 enrol/lti/tests/local/ltiadvantage/entity/user_test.php diff --git a/enrol/lti/classes/local/ltiadvantage/entity/ags_info.php b/enrol/lti/classes/local/ltiadvantage/entity/ags_info.php new file mode 100644 index 00000000000..929406f5b2a --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/ags_info.php @@ -0,0 +1,178 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * The ags_info class, instances of which represent grade service information for a resource_link or context. + * + * For information about Assignment and Grade Services 2.0, see https://www.imsglobal.org/spec/lti-ags/v2p0/. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ags_info { + /** @var string Scope for lineitem management, used when a platform allows the tool to create lineitems.*/ + private const SCOPES_LINEITEM_MANAGE = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; + + /** @var string Scope for lineitem reads, used when a tool only grants read access to line items.*/ + private const SCOPES_LINEITEM_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; + + /** @var string Scope for reading results.*/ + private const SCOPES_RESULT_READONLY = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; + + /** @var string Scope for posting scores.*/ + private const SCOPES_SCORES_POST = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; + + /** @var \moodle_url The service URL used to get/put lineitems*/ + private $lineitemsurl; + + /** @var \moodle_url|null The lineitemurl, which is only present when a single lineitem is supported.*/ + private $lineitemurl; + + /** @var array The array of supported lineitem-related scopes for this service instance.*/ + private $lineitemscopes = []; + + /** @var string|null The supported result scope for this service instance.*/ + private $resultscope = null; + + /** @var string|null The supported score scope for this service instance.*/ + private $scorescope = null; + + /** + * The ags_info constructor. + * + * @param \moodle_url $lineitemsurl The service URL used to get/put lineitems. + * @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported. + * @param array $scopes The array of supported scopes for this service instance. + */ + private function __construct(\moodle_url $lineitemsurl, ?\moodle_url $lineitemurl, array $scopes) { + $this->lineitemsurl = $lineitemsurl; + $this->lineitemurl = $lineitemurl; + $this->validate_scopes($scopes); + } + + /** + * Factory method to create a new ags_info instance. + * + * @param \moodle_url $lineitemsurl The service URL used to get/put lineitems. + * @param \moodle_url|null $lineitemurl The lineitemurl, which is only present when a single lineitem is supported. + * @param array $scopes The array of supported scopes for this service instance. + * @return ags_info the object instance. + */ + public static function create(\moodle_url $lineitemsurl, ?\moodle_url $lineitemurl = null, + array $scopes = []): ags_info { + return new self($lineitemsurl, $lineitemurl, $scopes); + } + + /** + * Check the supplied scopes for validity and set instance vars if appropriate. + * + * @param array $scopes the array of string scopes to check. + * @throws \coding_exception if any of the scopes is invalid. + */ + private function validate_scopes(array $scopes): void { + $validscopes = [ + self::SCOPES_LINEITEM_READONLY, + self::SCOPES_LINEITEM_MANAGE, + self::SCOPES_RESULT_READONLY, + self::SCOPES_SCORES_POST + ]; + foreach ($scopes as $scope) { + if (!is_string($scope)) { + throw new \coding_exception('Scope must be a string value'); + } + $key = array_search($scope, $validscopes); + if ($key === false) { + throw new \coding_exception("Scope '{$scope}' is invalid."); + } + if ($key == 0) { + $this->lineitemscopes[] = self::SCOPES_LINEITEM_READONLY; + } else if ($key == 1) { + $this->lineitemscopes[] = self::SCOPES_LINEITEM_MANAGE; + } else if ($key == 2) { + $this->resultscope = self::SCOPES_RESULT_READONLY; + } else if ($key == 3) { + $this->scorescope = self::SCOPES_SCORES_POST; + } + } + } + + /** + * Get the url for querying line items. + * + * @return \moodle_url the url. + */ + public function get_lineitemsurl(): \moodle_url { + return $this->lineitemsurl; + } + + /** + * Get the single line item url, in cases where only one line item exists. + * + * @return \moodle_url|null the url, or null if not present. + */ + public function get_lineitemurl(): ?\moodle_url { + return $this->lineitemurl; + } + + /** + * Get the authorization scope for lineitems. + * + * @return array|null the scopes, if present, else null. + */ + public function get_lineitemscope(): ?array { + return !empty($this->lineitemscopes) ? $this->lineitemscopes : null; + } + + /** + * Get the authorization scope for results. + * + * @return string|null the scope, if present, else null. + */ + public function get_resultscope(): ?string { + return $this->resultscope; + } + + /** + * Get the authorization scope for scores. + * + * @return string|null the scope, if present, else null. + */ + public function get_scorescope(): ?string { + return $this->scorescope; + } + + /** + * Get all supported scopes for this service. + * + * @return string[] the array of supported scopes. + */ + public function get_scopes(): array { + $scopes = []; + foreach ($this->lineitemscopes as $lineitemscope) { + $scopes[] = $lineitemscope; + } + if (!empty($this->resultscope)) { + $scopes[] = $this->resultscope; + } + if (!empty($this->scorescope)) { + $scopes[] = $this->scorescope; + } + return $scopes; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/application_registration.php b/enrol/lti/classes/local/ltiadvantage/entity/application_registration.php new file mode 100644 index 00000000000..acbc55e2db9 --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/application_registration.php @@ -0,0 +1,186 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class application_registration. + * + * This class represents an LTI Advantage Application Registration. + * Each registered application may contain one or more deployments of the Moodle tool. + * This registration provides the security contract for all tool deployments belonging to the registration. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class application_registration { + + /** @var int|null the if of this registration instance, or null if it hasn't been stored yet. */ + private $id; + + /** @var string the name of the application being registered. */ + private $name; + + /** @var \moodle_url the issuer identifying the platform, as provided by the platform. */ + private $platformid; + + /** @var string the client id as provided by the platform. */ + private $clientid; + + /** @var \moodle_url the authentication request URL, as provided by the platform. */ + private $authenticationrequesturl; + + /** @var \moodle_url the certificate URL, as provided by the platform. */ + private $jwksurl; + + /** @var \moodle_url the access token URL, as provided by the platform. */ + private $accesstokenurl; + + /** + * The application_registration constructor. + * + * @param string $name the descriptor for this application registration. + * @param \moodle_url $platformid the URL of application + * @param string $clientid unique id for the client on the application + * @param \moodle_url $authenticationrequesturl URL to send OIDC Auth requests to. + * @param \moodle_url $jwksurl URL to use to get public keys from the application. + * @param \moodle_url $accesstokenurl URL to use to get an access token from the application, used in service calls. + * @param int|null $id the id of the object instance, if being created from an existing store item. + */ + private function __construct(string $name, \moodle_url $platformid, string $clientid, + \moodle_url $authenticationrequesturl, \moodle_url $jwksurl, \moodle_url $accesstokenurl, int $id = null) { + + if (empty($name)) { + throw new \coding_exception("Invalid 'name' arg. Cannot be an empty string."); + } + if (empty($clientid)) { + throw new \coding_exception("Invalid 'clientid' arg. Cannot be an empty string."); + } + $this->name = $name; + $this->platformid = $platformid; + $this->clientid = $clientid; + $this->authenticationrequesturl = $authenticationrequesturl; + $this->jwksurl = $jwksurl; + $this->accesstokenurl = $accesstokenurl; + $this->id = $id; + } + + /** + * Factory method to create a new instance of an application repository + * + * @param string $name the descriptor for this application registration. + * @param \moodle_url $platformid the URL of application + * @param string $clientid unique id for the client on the application + * @param \moodle_url $authenticationrequesturl URL to send OIDC Auth requests to. + * @param \moodle_url $jwksurl URL to use to get public keys from the application. + * @param \moodle_url $accesstokenurl URL to use to get an access token from the application, used in service calls. + * @param int|null $id the id of the object instance, if being created from an existing store item. + * @return application_registration the application_registration instance. + */ + public static function create(string $name, \moodle_url $platformid, string $clientid, + \moodle_url $authenticationrequesturl, \moodle_url $jwksurl, \moodle_url $accesstokenurl, + int $id = null): application_registration { + + return new self($name, $platformid, $clientid, $authenticationrequesturl, $jwksurl, $accesstokenurl, $id); + } + + /** + * Get the integer id of this object instance. + * + * Will return null if the instance has not yet been stored. + * + * @return null|int the id, if set, otherwise null. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Get the name of the application being registered. + * + * @return string the name. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Get the platform id. + * + * @return \moodle_url the platformid/issuer URL. + */ + public function get_platformid(): \moodle_url { + return $this->platformid; + } + + /** + * Get the client id. + * + * @return string the client id. + */ + public function get_clientid(): string { + return $this->clientid; + } + + /** + * Get the authentication request URL. + * + * @return \moodle_url the authentication request URL. + */ + public function get_authenticationrequesturl(): \moodle_url { + return $this->authenticationrequesturl; + } + + /** + * Get the JWKS URL. + * + * @return \moodle_url the JWKS URL. + */ + public function get_jwksurl(): \moodle_url { + return $this->jwksurl; + } + + /** + * Get the access token URL. + * + * @return \moodle_url the access token URL. + */ + public function get_accesstokenurl(): \moodle_url { + return $this->accesstokenurl; + } + + /** + * Add a tool deployment to this registration. + * + * @param string $name human readable name for the deployment. + * @param string $deploymentid the unique id of the tool deployment in the platform. + * @return deployment the new deployment. + * @throws \coding_exception if trying to add a deployment to an instance without an id assigned. + */ + public function add_tool_deployment(string $name, string $deploymentid): deployment { + + if (empty($this->get_id())) { + throw new \coding_exception("Can't add deployment to a resource_link that hasn't first been saved."); + } + + return deployment::create( + $this->get_id(), + $deploymentid, + $name + ); + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/context.php b/enrol/lti/classes/local/ltiadvantage/entity/context.php new file mode 100644 index 00000000000..3d35fffcf75 --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/context.php @@ -0,0 +1,179 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class context, instances of which represent a context in the platform. + * + * See: http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary for supported context types. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +class context { + + // The following full contexts are per the spec: + // http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary. + /** @var string course template context */ + private const CONTEXT_TYPE_COURSE_TEMPLATE = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate'; + + /** @var string course offering context */ + private const CONTEXT_TYPE_COURSE_OFFERING = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering'; + + /** @var string course section context */ + private const CONTEXT_TYPE_COURSE_SECTION = 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'; + + /** @var string group context */ + private const CONTEXT_TYPE_GROUP = 'http://purl.imsglobal.org/vocab/lis/v2/course#Group'; + + // The following simple names are deprecated but are still supported in 1.3 for backwards compatibility. + // http://www.imsglobal.org/spec/lti/v1p3/#context-type-vocabulary. + /** @var string deprecated simple course template context */ + private const LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE = 'CourseTemplate'; + + /** @var string deprecated simple course offering context */ + private const LEGACY_CONTEXT_TYPE_COURSE_OFFERING = 'CourseOffering'; + + /** @var string deprecated simple course section context */ + private const LEGACY_CONTEXT_TYPE_COURSE_SECTION = 'CourseSection'; + + /** @var string deprecated simple group context */ + private const LEGACY_CONTEXT_TYPE_GROUP = 'Group'; + + /** @var int the local id of the deployment instance to which this context belongs. */ + private $deploymentid; + + /** @var string the contextid as supplied by the platform. */ + private $contextid; + + /** @var int|null the local id of this object instance, which can be null if the object hasn't been stored before */ + private $id; + + /** @var string[] the array of context types */ + private $types; + + /** + * Private constructor. + * + * @param int $deploymentid the local id of the deployment instance to which this context belongs. + * @param string $contextid the context id string, as provided by the platform during launch. + * @param array $types an array of string context types, as provided by the platform during launch. + * @param int|null $id local id of this object instance, nullable for new objects. + */ + private function __construct(int $deploymentid, string $contextid, array $types, ?int $id) { + if (!is_null($id) && $id <= 0) { + throw new \coding_exception('id must be a positive int'); + } + $this->deploymentid = $deploymentid; + $this->contextid = $contextid; + $this->set_types($types); // Handles type validation. + $this->id = $id; + } + + /** + * Factory method for creating a context instance. + * + * @param int $deploymentid the local id of the deployment instance to which this context belongs. + * @param string $contextid the context id string, as provided by the platform during launch. + * @param array $types an array of string context types, as provided by the platform during launch. + * @param int|null $id local id of this object instance, nullable for new objects. + * @return context the context instance. + */ + public static function create(int $deploymentid, string $contextid, array $types, int $id = null): context { + return new self($deploymentid, $contextid, $types, $id); + } + + /** + * Check whether a context is valid or not, checking also deprecated but supported legacy context names. + * + * @param string $type context type to check. + * @param bool $includelegacy whether to check the legacy simple context names too. + * @return bool true if the type is valid, false otherwise. + */ + private function is_valid_type(string $type, bool $includelegacy = false): bool { + // Check LTI Advantage types. + $valid = in_array($type, [ + self::CONTEXT_TYPE_COURSE_TEMPLATE, + self::CONTEXT_TYPE_COURSE_OFFERING, + self::CONTEXT_TYPE_COURSE_SECTION, + self::CONTEXT_TYPE_GROUP + ]); + + // Check legacy short names. + if ($includelegacy) { + $valid = $valid || in_array($type, [ + self::LEGACY_CONTEXT_TYPE_COURSE_TEMPLATE, + self::LEGACY_CONTEXT_TYPE_COURSE_OFFERING, + self::LEGACY_CONTEXT_TYPE_COURSE_SECTION, + self::LEGACY_CONTEXT_TYPE_GROUP + ]); + } + + return $valid; + } + + /** + * Get the object instance id. + * + * @return int|null the id, or null if the object doesn't yet have one assigned. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Return the platform contextid string. + * + * @return string the id of the context in the platform. + */ + public function get_contextid(): string { + return $this->contextid; + } + + /** + * Get the id of the local deployment instance to which this context instance belongs. + * + * @return int the id of the local deployment instance to which this context instance belongs. + */ + public function get_deploymentid(): int { + return $this->deploymentid; + } + + /** + * Get the context types this context instance represents. + * + * @return string[] the array of context types this context instance represents. + */ + public function get_types(): array { + return $this->types; + } + + /** + * Set the list of types this context instance represents. + * + * @param array $types the array of string types. + * @throws \coding_exception if any of the supplied types are invalid. + */ + public function set_types(array $types): void { + foreach ($types as $type) { + if (!$this->is_valid_type($type, true)) { + throw new \coding_exception("Cannot set invalid context type '{$type}'."); + } + } + $this->types = $types; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/deployment.php b/enrol/lti/classes/local/ltiadvantage/entity/deployment.php new file mode 100644 index 00000000000..ea4ad199b6b --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/deployment.php @@ -0,0 +1,178 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class deployment. + * + * This class represents an LTI Advantage Tool Deployment (http://www.imsglobal.org/spec/lti/v1p3/#tool-deployment). + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class deployment { + /** @var int|null the id of this object instance, or null if it has not been saved yet. */ + private $id; + + /** @var string the name of this deployment. */ + private $deploymentname; + + /** @var string The platform-issued deployment id. */ + private $deploymentid; + + /** @var int the local ID of the application registration to which this deployment belongs. */ + private $registrationid; + + /** @var string|null the legacy consumer key, if the deployment instance is migrated from a legacy consumer. */ + private $legacyconsumerkey; + + /** + * The private deployment constructor. + * + * @param string $deploymentname the name of this deployment. + * @param string $deploymentid the platform-issued deployment id. + * @param int $registrationid the local ID of the application registration. + * @param int|null $id the id of this object instance, or null if it is a new instance which has not yet been saved. + * @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades. + */ + private function __construct(string $deploymentname, string $deploymentid, int $registrationid, ?int $id = null, + ?string $legacyconsumerkey = null) { + + if (!is_null($id) && $id <= 0) { + throw new \coding_exception('id must be a positive int'); + } + if (empty($deploymentname)) { + throw new \coding_exception("Invalid 'deploymentname' arg. Cannot be an empty string."); + } + if (empty($deploymentid)) { + throw new \coding_exception("Invalid 'deploymentid' arg. Cannot be an empty string."); + } + $this->deploymentname = $deploymentname; + $this->deploymentid = $deploymentid; + $this->registrationid = $registrationid; + $this->id = $id; + $this->legacyconsumerkey = $legacyconsumerkey; + } + + /** + * Factory method to create a new instance of a deployment. + * + * @param int $registrationid the local ID of the application registration. + * @param string $deploymentid the platform-issued deployment id. + * @param string $deploymentname the name of this deployment. + * @param int|null $id optional local id of this object instance, omitted for new deployment objects. + * @param string|null $legacyconsumerkey the 1.1 consumer key associated with this deployment, used for upgrades. + * @return deployment the deployment instance. + */ + public static function create(int $registrationid, string $deploymentid, string $deploymentname, + ?int $id = null, ?string $legacyconsumerkey = null): deployment { + return new self($deploymentname, $deploymentid, $registrationid, $id, $legacyconsumerkey); + } + + /** + * Return the object id. + * + * @return int|null the id. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Return the short name of this tool deployment. + * + * @return string the short name. + */ + public function get_deploymentname(): string { + return $this->deploymentname; + } + + /** + * Get the deployment id string. + * + * @return string deploymentid + */ + public function get_deploymentid(): string { + return $this->deploymentid; + } + + /** + * Get the id of the application_registration. + * + * @return int the id of the application_registration instance to which this deployment belongs. + */ + public function get_registrationid(): int { + return $this->registrationid; + } + + /** + * Get the legacy consumer key to which this deployment instance is mapped. + * + * @return string|null the legacy consumer key, if set, else null. + */ + public function get_legacy_consumer_key(): ?string { + return $this->legacyconsumerkey; + } + + /** + * Factory method to add a platform-specific context to the deployment. + * + * @param string $contextid the contextid, as supplied by the platform during launch. + * @param array $types the context types the context represents, as supplied by the platform during launch. + * @return context the context instance. + * @throws \coding_exception if the context could not be created. + */ + public function add_context(string $contextid, array $types): context { + if (!$this->get_id()) { + throw new \coding_exception('Can\'t add context to a deployment that hasn\'t first been saved'); + } + + return context::create($this->get_id(), $contextid, $types); + } + + /** + * Factory method to create a resource link from this deployment instance. + * + * @param string $resourcelinkid the platform-issued string id of the resource link. + * @param int $resourceid the local published resource to which this link points. + * @param int|null $contextid the platform context instance in which the resource link resides, if available. + * @return resource_link the resource_link instance. + * @throws \coding_exception if the resource_link can't be created. + */ + public function add_resource_link(string $resourcelinkid, int $resourceid, + int $contextid = null): resource_link { + + if (!$this->get_id()) { + throw new \coding_exception('Can\'t add resource_link to a deployment that hasn\'t first been saved'); + } + return resource_link::create($resourcelinkid, $this->get_id(), $resourceid, $contextid); + } + + /** + * Set the legacy consumer key for this instance, indicating that the deployment has been migrated from a consumer. + * + * @param string $key the legacy consumer key. + * @throws \coding_exception if the key is invalid. + */ + public function set_legacy_consumer_key(string $key): void { + if (strlen($key) > 255) { + throw new \coding_exception('Legacy consumer key too long. Cannot exceed 255 chars.'); + } + $this->legacyconsumerkey = $key; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/migration_claim.php b/enrol/lti/classes/local/ltiadvantage/entity/migration_claim.php new file mode 100644 index 00000000000..5d1926b8a10 --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/migration_claim.php @@ -0,0 +1,191 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; + +/** + * The migration_claim class, instances of which represent information passed in an 'lti1p1' migration claim. + * + * Provides validation and data retrieval for the claim. + * + * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class migration_claim { + + /** @var string the LTI 1.1 consumer key */ + private $consumerkey; + + /** @var string the LTI 1.1 user identifier. + * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim. + * I.e. https://www.imsglobal.org/spec/security/v1p0#id-token + */ + private $userid = null; + + /** @var string the LTI 1.1 context identifier. + * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'context' claim. + * I.e. https://purl.imsglobal.org/spec/lti/claim/context#id. + */ + private $contextid = null; + + /** @var string the LTI 1.1 consumer instance GUID. + * This is only included in the claim if it differs to the 'guid' property of the LTI 1.3 'tool_platform' claim. + * I.e. https://purl.imsglobal.org/spec/lti/claim/tool_platform#guid. + */ + private $toolconsumerinstanceguid = null; + + /** @var string the LTI 1.1 resource link identifier. + * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'resource_link' claim. + * I.e. https://purl.imsglobal.org/spec/lti/claim/resource_link#id. + */ + private $resourcelinkid = null; + + /** @var legacy_consumer_repository repository instance for querying consumer secrets when verifying signature. */ + private $legacyconsumerrepo; + + /** + * The migration_claim constructor. + * + * @param array $claim the array of claim data, as received in a resource link launch. + * @param string $deploymentid the deployment id included in the launch. + * @param string $platform the platform included in the launch. + * @param string $clientid the client id included in the launch. + * @param string $exp the exp included in the launch. + * @param string $nonce the nonce included in the launch. + * @param legacy_consumer_repository $legacyconsumerrepo a legacy consumer repository instance. + * @throws \coding_exception if the claim data is invalid. + */ + public function __construct(array $claim, string $deploymentid, string $platform, string $clientid, string $exp, + string $nonce, legacy_consumer_repository $legacyconsumerrepo) { + + // The oauth_consumer_key property MUST be sent. + // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key. + if (empty($claim['oauth_consumer_key'])) { + throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim."); + } + + // The oauth_consumer_key_sign property MAY be sent. + // For user migration to take place, however, this is deemed a required property. + // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign. + if (empty($claim['oauth_consumer_key_sign'])) { + throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."); + } + $this->legacyconsumerrepo = $legacyconsumerrepo; + + if (!$this->verify_signature($claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $deploymentid, + $platform, $clientid, $exp, $nonce, $legacyconsumerrepo)) { + throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim."); + } + + $this->consumerkey = $claim['oauth_consumer_key']; + $this->userid = $claim['user_id'] ?? null; + $this->contextid = $claim['context_id'] ?? null; + $this->toolconsumerinstanceguid = $claim['tool_consumer_instance_guid'] ?? null; + $this->resourcelinkid = $claim['resource_link_id'] ?? null; + } + + /** + * Verify the claim signature by recalculating it using the launch data and locally stored consumer secret. + * + * @param string $consumerkey the LTI 1.1 consumer key. + * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data. + * @param string $deploymentid the deployment id included in the launch. + * @param string $platform the platform included in the launch. + * @param string $clientid the client id included in the launch. + * @param string $exp the exp included in the launch. + * @param string $nonce the nonce included in the launch. + * @return bool true if the signature was verified, false otherwise. + */ + private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform, + string $clientid, string $exp, string $nonce): bool { + + $base = [ + $consumerkey, + $deploymentid, + $platform, + $clientid, + $exp, + $nonce + ]; + $basestring = implode('&', $base); + + // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in + // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's + // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the + // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found + // or no signatures match. + $consumersecrets = $this->legacyconsumerrepo->get_consumer_secrets($consumerkey); + foreach ($consumersecrets as $consumersecret) { + $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret)); + + if ($signature === $calculatedsignature) { + return true; + } + } + return false; + } + + /** + * Return the consumer key stored in the claim. + * + * @return string the consumer key included in the claim. + */ + public function get_consumer_key(): string { + return $this->consumerkey; + } + + /** + * Return the LTI 1.1 user id stored in the claim. + * + * @return string|null the user id, or null if not provided in the claim. + */ + public function get_user_id(): ?string { + return $this->userid; + } + + + /** + * Return the LTI 1.1 context id stored in the claim. + * + * @return string|null the context id, or null if not provided in the claim. + */ + public function get_context_id(): ?string { + return $this->contextid; + } + + /** + * Return the LTI 1.1 tool consumer instance GUID stored in the claim. + * + * @return string|null the tool consumer instance GUID, or null if not provided in the claim. + */ + public function get_tool_consumer_instance_guid(): ?string { + return $this->toolconsumerinstanceguid; + } + + /** + * Return the LTI 1.1 resource link id stored in the claim. + * + * @return string|null the resource link id, or null if not provided in the claim. + */ + public function get_resource_link_id(): ?string { + return $this->resourcelinkid; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/nrps_info.php b/enrol/lti/classes/local/ltiadvantage/entity/nrps_info.php new file mode 100644 index 00000000000..d267f66fd7e --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/nrps_info.php @@ -0,0 +1,131 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class nrps_info, instances of which represent a names and roles provisioning service for a resource. + * + * For information about Names and Role Provisioning Services 2.0, see http://www.imsglobal.org/spec/lti-nrps/v2p0. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +class nrps_info { + + /** @var \moodle_url the memberships URL for the service. */ + private $contextmembershipsurl; + + /** @var float[] the array of supported service versions. */ + private $serviceversions; + + // Service versions are specified by the platform during launch. + // See http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration. + /** @var string version 1.0 */ + private const SERVICE_VERSION_1 = '1.0'; + + /** @var string version 2.0 */ + private const SERVICE_VERSION_2 = '2.0'; + + // Scope that must be requested as part of making a service call. + // See: http://www.imsglobal.org/spec/lti-nrps/v2p0#lti-1-3-integration. + /** @var string the scope to request to make service calls. */ + private $servicescope = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; + + /** + * The private nrps_info constructor. + * + * @param \moodle_url $contextmembershipsurl the memberships URL. + * @param string[] $serviceversions the supported service versions. + */ + private function __construct(\moodle_url $contextmembershipsurl, array $serviceversions = [self::SERVICE_VERSION_2]) { + $this->contextmembershipsurl = $contextmembershipsurl; + $this->set_service_versions($serviceversions); + } + + /** + * Factory method to create a new nrps_info instance. + * + * @param \moodle_url $contextmembershipsurl the memberships URL. + * @param string[] $serviceversions the supported service versions. + * @return nrps_info the object instance. + */ + public static function create(\moodle_url $contextmembershipsurl, + array $serviceversions = [self::SERVICE_VERSION_2]): nrps_info { + return new self($contextmembershipsurl, $serviceversions); + } + + /** + * Check whether the supplied service version is valid or not. + * + * @param string $serviceversion the service version to check. + * @return bool true if valid, false otherwise. + */ + private function is_valid_service_version(string $serviceversion): bool { + $validversions = [ + self::SERVICE_VERSION_1, + self::SERVICE_VERSION_2 + ]; + + return in_array($serviceversion, $validversions); + } + + /** + * Tries to set the supported service versions for this instance. + * + * @param array $serviceversions the service versions to set. + * @throws \coding_exception if any of the supplied versions are not valid. + */ + private function set_service_versions(array $serviceversions): void { + if (empty($serviceversions)) { + throw new \coding_exception('Service versions array cannot be empty'); + } + $serviceversions = array_unique($serviceversions); + foreach ($serviceversions as $serviceversion) { + if (!$this->is_valid_service_version($serviceversion)) { + throw new \coding_exception("Invalid Names and Roles service version '{$serviceversion}'"); + } + } + $this->serviceversions = $serviceversions; + } + + /** + * Get the service URL for this grade service instance. + * + * @return \moodle_url the service URL. + */ + public function get_context_memberships_url(): \moodle_url { + return $this->contextmembershipsurl; + } + + /** + * Get the supported service versions for this grade service instance. + * + * @return string[] the array of supported service versions. + */ + public function get_service_versions(): array { + return $this->serviceversions; + } + + /** + * Get the nrps service scope. + * + * @return string the service scope. + */ + public function get_service_scope(): string { + return $this->servicescope; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/registration_url.php b/enrol/lti/classes/local/ltiadvantage/entity/registration_url.php new file mode 100644 index 00000000000..33d10d848f7 --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/registration_url.php @@ -0,0 +1,69 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; +use moodle_url; + +/** + * Class registration_url, representing a single dynamic registration URL. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class registration_url extends moodle_url { + + /** @var string the address of the registration URL. */ + protected $address; + + /** @var string the random token used to secure this registration URL. */ + protected $token; + + /** @var int Unix time at which this registration URL is no longer valid. */ + protected $expirytime; + + /** + * Constructor. + * + * @param int $expirytime the unix time after which the URL is deemed invalid. + * @param string|null $token the unique token securing requests to the URL. + * @throws \coding_exception if the token or expiry time is invalid. + */ + public function __construct(int $expirytime, string $token = null) { + global $CFG; + if ($expirytime < 0) { + throw new \coding_exception('Invalid registration_url expiry time. Must be greater than or equal to 0.'); + } + $this->address = $CFG->wwwroot . '/enrol/lti/register.php'; + $this->expirytime = $expirytime; + if (is_null($token)) { + $bytes = random_bytes(30); + $token = bin2hex($bytes); + } + $this->token = $token; + + parent::__construct($this->address, ['token' => $this->token], null); + } + + /** + * Get the expiry time of this registration_url instance. + * + * @return int the unix time of the expiry. + */ + public function get_expiry_time(): int { + return $this->expirytime; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/resource_link.php b/enrol/lti/classes/local/ltiadvantage/entity/resource_link.php new file mode 100644 index 00000000000..d6e0a81e9fe --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/resource_link.php @@ -0,0 +1,231 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class resource_link. + * + * This class represents an LTI Advantage Resource Link (http://www.imsglobal.org/spec/lti/v1p3/#resource-link). + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class resource_link { + + /** @var int|null the id of this object, or null if the object hasn't been stored yet. */ + private $id; + + /** @var string resourcelinkid the id of the resource link as supplied by the platform. */ + private $resourcelinkid; + + /** @var int $deploymentid the local id of the deployment instance to which this resource link belongs. */ + private $deploymentid; + + /** @var int|null $contextid the id of local context object representing the platform context, or null. */ + private $contextid; + + /** @var int The id of the local published resource this resource_link points to. */ + private $resourceid; + + /** @var ags_info|null the grade service for this resource_link, null if not applicable/not provided. */ + private $gradeservice; + + /** @var nrps_info|null the names and roles service for this resource_link, null if not applicable/not provided. */ + private $namesrolesservice; + + /** + * The private resource_link constructor. + * + * @param string $resourcelinkid the id of the resource link as supplied by the platform. + * @param int $deploymentid the local id of the deployment instance to which this resource link belongs. + * @param int $resourceid the id of the local resource to which this link refers. + * @param int|null $contextid the id local context object representing the context within the platform. + * @param int|null $id the local id of this resource_link object. + * @throws \coding_exception if the instance is unable to be created. + */ + private function __construct(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null, + int $id = null) { + + if (empty($resourcelinkid)) { + throw new \coding_exception('Error: resourcelinkid cannot be an empty string'); + } + $this->resourcelinkid = $resourcelinkid; + $this->deploymentid = $deploymentid; + $this->resourceid = $resourceid; + $this->contextid = $contextid; + $this->id = $id; + $this->gradeservice = null; + $this->namesrolesservice = null; + } + + /** + * Factory method to create an instance. + * + * @param string $resourcelinkid the resourcelinkid, as provided by the platform. + * @param int $deploymentid the local id of the deployment to which this resource link belongs. + * @param int $resourceid the id of the local resource this resource_link refers to. + * @param int|null $contextid the id of the local context object representing the platform context. + * @param int|null $id the local id of the resource link instance. + * @return resource_link the newly created instance. + */ + public static function create(string $resourcelinkid, int $deploymentid, int $resourceid, ?int $contextid = null, + int $id = null): resource_link { + + return new self($resourcelinkid, $deploymentid, $resourceid, $contextid, $id); + } + + /** + * Return the id of this object instance. + * + * @return null|int the id or null if the object has not yet been stored. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Get the resourcelinkid as provided by the platform. + * + * @return string the resourcelinkid. + */ + public function get_resourcelinkid(): string { + return $this->resourcelinkid; + } + + /** + * Return the id of the deployment to which this resource link belongs. + * + * This id is the local id of the deployment instance, not the deploymentid provided by the platform. + * + * @return int the deployment id. + */ + public function get_deploymentid(): int { + return $this->deploymentid; + } + + /** + * Get the local id of the published resource to which this resource link refers. + * + * @return int the id of the published resource. + */ + public function get_resourceid(): int { + return $this->resourceid; + } + + /** + * Return the id of the context object holding information about where this resource link resides. + * + * @return int|null the id or null if not present. + */ + public function get_contextid(): ?int { + return $this->contextid; + } + + /** + * Link this resource_link instance with a context. + * + * @param int $contextid the local id of the context instance containing information about the platform context. + */ + public function set_contextid(int $contextid): void { + if ($contextid <= 0) { + throw new \coding_exception('Context id must be a positive int'); + } + $this->contextid = $contextid; + } + + /** + * Set which local published resource this resource link refers to. + * + * @param int $resourceid the published resource id. + */ + public function set_resourceid(int $resourceid): void { + if ($resourceid <= 0) { + throw new \coding_exception('Resource id must be a positive int'); + } + $this->resourceid = $resourceid; + } + + /** + * Add grade service information to this resource_link instance. + * + * @param \moodle_url $lineitemsurl the service URL for get/put of line items. + * @param \moodle_url|null $lineitemurl the service URL if only a single line item is present in the platform. + * @param string[] $scopes the string array of grade service scopes which may be used by the service. + */ + public function add_grade_service(\moodle_url $lineitemsurl, ?\moodle_url $lineitemurl = null, array $scopes = []) { + $this->gradeservice = ags_info::create($lineitemsurl, $lineitemurl, $scopes); + } + + /** + * Get the grade service attached to this resource_link instance, or null if there isn't one associated. + * + * @return ags_info|null the grade service object instance, or null if not found. + */ + public function get_grade_service(): ?ags_info { + return $this->gradeservice; + } + + /** + * Add names and roles service information to this resource_link instance. + * + * @param \moodle_url $contextmembershipurl the service URL for memberships. + * @param string[] $serviceversions the string array of supported service versions. + */ + public function add_names_and_roles_service(\moodle_url $contextmembershipurl, array $serviceversions): void { + $this->namesrolesservice = nrps_info::create($contextmembershipurl, $serviceversions); + } + + /** + * Get the names and roles service attached to this resource_link instance, or null if there isn't one associated. + * + * @return nrps_info|null the names and roles service object instance, or null if not found. + */ + public function get_names_and_roles_service(): ?nrps_info { + return $this->namesrolesservice; + } + + /** + * Factory method to create a user from this resource_link instance. + * + * This is useful for associating the user with the resource link and resource I.e. the user was created when + * launching a specific resource link. + * + * @param int $userid the id of the Moodle user record. + * @param string $sourceid the id of the user on the platform. + * @param string $lang the user's lang code. + * @param string $city the user's city. + * @param string $country the user's country. + * @param string $institution the user's institution. + * @param string $timezone the user's timezone. + * @param int|null $maildisplay the user's maildisplay, which can be omitted to use sensible defaults. + * @return user the user instance. + * @throws \coding_exception if trying to add a user to an as-yet-unsaved resource_link instance. + */ + public function add_user(int $userid, string $sourceid, string $lang, + string $city, string $country, string $institution, string $timezone, + ?int $maildisplay = null): user { + + if (empty($this->get_id())) { + throw new \coding_exception('Can\'t add user to a resource_link that hasn\'t first been saved'); + } + + return user::create_from_resource_link($this->get_id(), $this->get_resourceid(), $userid, + $this->get_deploymentid(), $sourceid, $lang, $timezone, $city, $country, + $institution, $maildisplay); + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/entity/user.php b/enrol/lti/classes/local/ltiadvantage/entity/user.php new file mode 100644 index 00000000000..8b6ca6330ec --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/entity/user.php @@ -0,0 +1,420 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Class user, instances of which represent a specific lti user in the tool. + * + * A user is always associated with a resource, as lti users cannot exist without a tool-published-resource. A user will + * always come from either: + * - a resource link launch or + * - a membership sync + * Both of which required a published resource. + * + * Additionally, a user may be associated with a given resource_link instance, to signify that the user originated from + * that resource_link. If a user is not associated with a resource_link, such as when creating a user during a member + * sync, that param is nullable. This can be achieved via the factory method user::create_from_resource_link() or set + * after instantiation via the user::set_resource_link_id() method. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +class user { + + /** @var int the id of the published resource to which this user belongs. */ + private $resourceid; + + /** @var int the local id of the deployment instance to which this user belongs. */ + private $deploymentid; + + /** @var string the id of the user in the platform site. */ + private $sourceid; + + /** @var int|null the id of this user instance, or null if not stored yet. */ + private $id; + + /** @var int|null the id of the user in the tool site, or null if the instance hasn't been stored yet. */ + private $localid; + + /** @var string city of the user. */ + private $city; + + /** @var string country of the user. */ + private $country; + + /** @var string institution of the user.*/ + private $institution; + + /** @var string timezone of the user. */ + private $timezone; + + /** @var int maildisplay of the user. */ + private $maildisplay; + + /** @var string language code of the user. */ + private $lang; + + /** @var float the user's last grade value. */ + private $lastgrade; + + /** @var int|null the user's last access unix timestamp, or null if they have not accessed the resource yet.*/ + private $lastaccess; + + /** @var int|null the id of the resource_link instance, or null if the user doesn't originate from one. */ + private $resourcelinkid; + + /** + * Private constructor. + * + * @param int $resourceid the id of the published resource to which this user belongs. + * @param int $userid the id of the Moodle user to which this LTI user relates. + * @param int $deploymentid the local id of the deployment instance to which this user belongs. + * @param string $sourceid the id of the user in the platform site. + * @param string $lang the user's language code. + * @param string $city the user's city. + * @param string $country the user's country. + * @param string $institution the user's institution. + * @param string $timezone the user's timezone. + * @param int|null $maildisplay the user's maildisplay, or null to select defaults. + * @param float|null $lastgrade the user's last grade value. + * @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource. + * @param int|null $resourcelinkid the id of the resource link to link to the user, or null if not applicable. + * @param int|null $id the id of this object instance, or null if it's a not-yet-persisted object. + * @throws \coding_exception + */ + private function __construct(int $resourceid, int $userid, int $deploymentid, string $sourceid, + string $lang, string $city, string $country, + string $institution, string $timezone, ?int $maildisplay, ?float $lastgrade, ?int $lastaccess, + ?int $resourcelinkid = null, ?int $id = null) { + + global $CFG; + $this->resourceid = $resourceid; + $this->localid = $userid; + $this->deploymentid = $deploymentid; + if (empty($sourceid)) { + throw new \coding_exception('Invalid sourceid value. Cannot be an empty string.'); + } + $this->sourceid = $sourceid; + $this->set_lang($lang); + $this->set_city($city); + $this->set_country($country); + $this->set_institution($institution); + $this->set_timezone($timezone); + $maildisplay = $maildisplay ?? ($CFG->defaultpreference_maildisplay ?? 2); + $this->set_maildisplay($maildisplay); + $this->lastgrade = $lastgrade ?? 0.0; + $this->lastaccess = $lastaccess; + $this->resourcelinkid = $resourcelinkid; + $this->id = $id; + } + + /** + * Factory method for creating a user instance associated with a given resource_link instance. + * + * @param int $resourcelinkid the local id of the resource link instance to link to the user. + * @param int $resourceid the id of the published resource to which this user belongs. + * @param int $userid the id of the Moodle user to which this LTI user relates. + * @param int $deploymentid the local id of the deployment instance to which this user belongs. + * @param string $sourceid the id of the user in the platform site. + * @param string $lang the user's language code. + * @param string $timezone the user's timezone. + * @param string $city the user's city. + * @param string $country the user's country. + * @param string $institution the user's institution. + * @param int|null $maildisplay the user's maildisplay, or null to select defaults. + * @return user the user instance. + */ + public static function create_from_resource_link(int $resourcelinkid, int $resourceid, int $userid, + int $deploymentid, string $sourceid, string $lang, string $timezone, + string $city = '', string $country = '', string $institution = '', + ?int $maildisplay = null): user { + + return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city, + $country, $institution, $timezone, $maildisplay, null, null, $resourcelinkid); + } + + /** + * Factory method for creating a user. + * + * @param int $resourceid the id of the published resource to which this user belongs. + * @param int $userid the id of the Moodle user to which this LTI user relates. + * @param int $deploymentid the local id of the deployment instance to which this user belongs. + * @param string $sourceid the id of the user in the platform site. + * @param string $lang the user's language code. + * @param string $timezone the user's timezone. + * @param string $city the user's city. + * @param string $country the user's country. + * @param string $institution the user's institution. + * @param int|null $maildisplay the user's maildisplay, or null to select defaults. + * @param float|null $lastgrade the user's last grade value. + * @param int|null $lastaccess the user's last access time, or null if they haven't accessed the resource. + * @param int|null $resourcelinkid the local id of the resource link instance associated with the user. + * @param int|null $id the id of this lti user instance, or null if it's a not-yet-persisted object. + * @return user the user instance. + */ + public static function create(int $resourceid, int $userid, int $deploymentid, string $sourceid, + string $lang, string $timezone, string $city = '', + string $country = '', string $institution = '', ?int $maildisplay = null, ?float $lastgrade = null, + ?int $lastaccess = null, ?int $resourcelinkid = null, int $id = null): user { + + return new self($resourceid, $userid, $deploymentid, $sourceid, $lang, $city, + $country, $institution, $timezone, $maildisplay, $lastgrade, $lastaccess, $resourcelinkid, $id); + } + + /** + * Get the id of this user instance. + * + * @return int|null the object id, or null if not yet persisted. + */ + public function get_id(): ?int { + return $this->id; + } + + /** + * Get the id of the resource_link instance to which this user is associated. + * + * @return int|null the object id, or null if the user isn't associated with one. + */ + public function get_resourcelinkid(): ?int { + return $this->resourcelinkid; + } + + /** + * Associate this user with the given resource_link instance, denoting that this user launched from the instance. + * + * @param int $resourcelinkid the id of the resource_link instance. + */ + public function set_resourcelinkid(int $resourcelinkid): void { + if ($resourcelinkid <= 0) { + throw new \coding_exception("Invalid resourcelinkid '$resourcelinkid' provided. Must be > 0."); + } + $this->resourcelinkid = $resourcelinkid; + } + + /** + * Get the id of the published resource to which this user is associated. + * + * @return int the resource id. + */ + public function get_resourceid(): int { + return $this->resourceid; + } + + /** + * Get the id of the deployment instance to which this user belongs. + * + * @return int id of the deployment instance. + */ + public function get_deploymentid(): int { + return $this->deploymentid; + } + + /** + * Get the id of the user in the platform. + * + * @return string the source id. + */ + public function get_sourceid(): string { + return $this->sourceid; + } + + /** + * Get the id of the user in the tool. + * + * @return int|null the id, or null if the object instance hasn't yet been persisted. + */ + public function get_localid(): ?int { + return $this->localid; + } + + /** + * Get the user's city. + * + * @return string the city. + */ + public function get_city(): string { + return $this->city; + } + + /** + * Set the user's city. + * + * @param string $city the city string. + */ + public function set_city(string $city): void { + $this->city = $city; + } + + /** + * Get the user's country code. + * + * @return string the country code. + */ + public function get_country(): string { + return $this->country; + } + + /** + * Set the user's country. + * + * @param string $countrycode the 2 digit country code representing the country, or '' to denote none. + */ + public function set_country(string $countrycode): void { + global $CFG; + require_once($CFG->libdir . '/moodlelib.php'); + $validcountrycodes = array_merge([''], array_keys(get_string_manager()->get_list_of_countries(true))); + if (!in_array($countrycode, $validcountrycodes)) { + throw new \coding_exception("Invalid country code '$countrycode'."); + } + $this->country = $countrycode; + } + + /** + * Get the instituation of the user. + * + * @return string the institution. + */ + public function get_institution(): string { + return $this->institution; + } + + /** + * Set the user's institution. + * + * @param string $institution the name of the institution. + */ + public function set_institution(string $institution): void { + $this->institution = $institution; + } + + /** + * Get the timezone of the user. + * + * @return string the user timezone. + */ + public function get_timezone(): string { + return $this->timezone; + } + + /** + * Set the user's timezone, or set '99' to specify server timezone. + * + * @param string $timezone the timezone string, or '99' to use server timezone. + */ + public function set_timezone(string $timezone): void { + if (empty($timezone)) { + throw new \coding_exception('Invalid timezone value. Cannot be an empty string.'); + } + $validtimezones = array_keys(\core_date::get_list_of_timezones(null, true)); + if (!in_array($timezone, $validtimezones)) { + throw new \coding_exception("Invalid timezone '$timezone' provided."); + } + $this->timezone = $timezone; + } + + /** + * Get the maildisplay of the user. + * + * @return int the maildisplay. + */ + public function get_maildisplay(): int { + return $this->maildisplay; + } + + /** + * Set the user's mail display preference from a range of supported options. + * + * 0 - hide from non privileged users + * 1 - allow everyone to see + * 2 - allow only course participants to see + * + * @param int $maildisplay the maildisplay preference to set. + */ + public function set_maildisplay(int $maildisplay): void { + if (!in_array($maildisplay, range(0, 2))) { + throw new \coding_exception("Invalid maildisplay value '$maildisplay'. Must be in the range {0..2}."); + } + $this->maildisplay = $maildisplay; + } + + /** + * Get the lang code of the user. + * + * @return string the user's language code. + */ + public function get_lang(): string { + return $this->lang; + } + + /** + * Set the user's language. + * + * @param string $langcode the language code representing the user's language. + */ + public function set_lang(string $langcode): void { + if (empty($langcode)) { + throw new \coding_exception('Invalid lang value. Cannot be an empty string.'); + } + $validlangcodes = array_keys(get_string_manager()->get_list_of_translations()); + if (!in_array($langcode, $validlangcodes)) { + throw new \coding_exception("Invalid lang '$langcode' provided."); + } + $this->lang = $langcode; + } + + /** + * Get the last grade value for this user. + * + * @return float the float grade. + */ + public function get_lastgrade(): float { + return $this->lastgrade; + } + + /** + * Set the last grade for the user. + * + * @param float $lastgrade the last grade the user received. + */ + public function set_lastgrade(float $lastgrade): void { + global $CFG; + require_once($CFG->libdir . '/gradelib.php'); + $this->lastgrade = grade_floatval($lastgrade); + } + + /** + * Get the last access timestamp for this user. + * + * @return int|null the last access timestampt, or null if the user hasn't accessed the resource. + */ + public function get_lastaccess(): ?int { + return $this->lastaccess; + } + + /** + * Set the last access time for the user. + * + * @param int $time unix timestamp representing the last time the user accessed the published resource. + * @throws \coding_exception if $time is not a positive int. + */ + public function set_lastaccess(int $time): void { + if ($time < 0) { + throw new \coding_exception('Cannot set negative access time'); + } + $this->lastaccess = $time; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/ags_info_test.php b/enrol/lti/tests/local/ltiadvantage/entity/ags_info_test.php new file mode 100644 index 00000000000..689e816b0d4 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/ags_info_test.php @@ -0,0 +1,276 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for ags_info. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\ags_info + */ +class ags_info_test extends \advanced_testcase { + + /** + * Test creation of the object instances. + * @dataProvider instantiation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_creation(array $args, array $expectations) { + if (!$expectations['valid']) { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + ags_info::create(...array_values($args)); + } else { + $agsinfo = ags_info::create(...array_values($args)); + $this->assertEquals($args['lineitemsurl'], $agsinfo->get_lineitemsurl()); + $this->assertEquals($args['lineitemurl'], $agsinfo->get_lineitemurl()); + $this->assertEquals($args['scopes'], $agsinfo->get_scopes()); + $this->assertEquals($expectations['lineitemscope'], $agsinfo->get_lineitemscope()); + $this->assertEquals($expectations['scorescope'], $agsinfo->get_scorescope()); + $this->assertEquals($expectations['resultscope'], $agsinfo->get_resultscope()); + } + } + + /** + * Data provider for testing object instantiation. + * @return array the data for testing. + */ + public function instantiation_data_provider(): array { + return [ + 'Both lineitems and lineitem URL provided with full list of valid scopes' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' + ], + 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'Both lineitems and lineitem URL provided with lineitem scopes only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + ], + 'scorescope' => null, + 'resultscope' => null + ] + ], + 'Both lineitems and lineitem URL provided with score scope only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score', + 'resultscope' => null + ] + ], + 'Both lineitems and lineitem URL provided with result scope only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => null, + 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' + ] + ], + 'Both lineitems and lineitem URL provided with no scopes' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => null, + 'resultscope' => null + ] + ], + 'Just lineitems URL, no lineitem URL, with full list of valid scopes' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => null, + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' + ], + 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'Just lineitems URL, no lineitem URL, with lineitems scopes only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => null, + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + ], + 'scorescope' => null, + 'resultscope' => null + ] + ], + 'Just lineitems URL, no lineitem URL, with score scope only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => null, + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/score' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/score', + 'resultscope' => null + ] + ], + 'Just lineitems URL, no lineitem URL, with result scope only' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => null, + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' + ] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => null, + 'resultscope' => 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly' + ] + ], + 'Just lineitems URL, no lineitem URL, with no scopes' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => null, + 'scopes' => [] + ], + 'expectations' => [ + 'valid' => true, + 'lineitemscope' => null, + 'scorescope' => null, + 'resultscope' => null + ] + ], + 'Both lineitems and lineitem URL provided with non-string scope' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/score', + 12345 + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Scope must be a string value' + ] + ], + 'Both lineitems and lineitem URL provided with invalid scopes' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', + 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', + 'https://purl.imsglobal.org/spec/lti-ags/scope/score', + 'https://example.com/invalid/scope' + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Scope 'https://example.com/invalid/scope' is invalid." + ] + ], + 'Both lineitems and lineitem URL provided with invalid scope types' => [ + 'args' => [ + 'lineitemsurl' => new \moodle_url('https://platform.example.org/10/lineitems'), + 'lineitemurl' => new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + 'scopes' => [ + 12 + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Scope must be a string value" + ] + ], + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/application_registration_test.php b/enrol/lti/tests/local/ltiadvantage/entity/application_registration_test.php new file mode 100644 index 00000000000..4f84b370550 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/application_registration_test.php @@ -0,0 +1,241 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for application_registration. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\application_registration + */ +class application_registration_test extends \advanced_testcase { + + /** + * Test the creation of an application_registration instance. + * + * @dataProvider creation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_creation(array $args, array $expectations) { + if ($expectations['valid']) { + $reg = application_registration::create(...array_values($args)); + $this->assertEquals($args['name'], $reg->get_name()); + $this->assertEquals($args['platformid'], $reg->get_platformid()); + $this->assertEquals($args['clientid'], $reg->get_clientid()); + $this->assertEquals($args['authrequesturl'], $reg->get_authenticationrequesturl()); + $this->assertEquals($args['jwksurl'], $reg->get_jwksurl()); + $this->assertEquals($args['accesstokenurl'], $reg->get_accesstokenurl()); + $expectedid = $args['id'] ?? null; + $this->assertEquals($expectedid, $reg->get_id()); + } else { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + application_registration::create(...array_values($args)); + } + } + + /** + * Data provider for testing the creation of application_registration instances. + * + * @return array the data for testing. + */ + public function creation_data_provider(): array { + return [ + 'Valid, only required args provided' => [ + 'args' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + ], + 'expectations' => [ + 'valid' => true + ] + ], + 'Valid, all args provided' => [ + 'args' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + 'id' => 24 + ], + 'expectations' => [ + 'valid' => true + ] + ], + 'Invalid, empty name provided' => [ + 'args' => [ + 'name' => '', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'name' arg. Cannot be an empty string." + ] + ], + 'Invalid, empty clientid provided' => [ + 'args' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => '', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'clientid' arg. Cannot be an empty string." + ] + ] + ]; + } + + /** + * Test the factory method for creating a tool deployment associated with the registration instance. + * + * @dataProvider add_tool_deployment_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::add_tool_deployment + */ + public function test_add_tool_deployment(array $args, array $expectations) { + + if ($expectations['valid']) { + $reg = application_registration::create(...array_values($args['registration'])); + $deployment = $reg->add_tool_deployment(...array_values($args['deployment'])); + $this->assertInstanceOf(deployment::class, $deployment); + $this->assertEquals($args['deployment']['name'], $deployment->get_deploymentname()); + $this->assertEquals($args['deployment']['deploymentid'], $deployment->get_deploymentid()); + $this->assertEquals($reg->get_id(), $deployment->get_registrationid()); + } else { + $reg = application_registration::create(...array_values($args['registration'])); + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + $reg->add_tool_deployment(...array_values($args['deployment'])); + } + } + + /** + * Data provider for testing the add_tool_deployment method. + * + * @return array the array of test data. + */ + public function add_tool_deployment_data_provider(): array { + return [ + 'Valid, contains id on registration and valid deployment data provided' => [ + 'args' => [ + 'registration' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + 'id' => 24 + ], + 'deployment' => [ + 'name' => 'Deployment at site level', + 'deploymentid' => 'id-123abc' + ] + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Invalid, missing id on registration' => [ + 'args' => [ + 'registration' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + ], + 'deployment' => [ + 'name' => 'Deployment at site level', + 'deploymentid' => 'id-123abc' + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Can't add deployment to a resource_link that hasn't first been saved." + ] + ], + 'Invalid, id present on registration but empty deploymentname' => [ + 'args' => [ + 'registration' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + 'id' => 24 + ], + 'deployment' => [ + 'name' => '', + 'deploymentid' => 'id-123abc' + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'deploymentname' arg. Cannot be an empty string." + ] + ], + 'Invalid, id present on registration but empty deploymentid' => [ + 'args' => [ + 'registration' => [ + 'name' => 'Platform X', + 'platformid' => new \moodle_url('https://lms.example.com'), + 'clientid' => 'client-id-12345', + 'authrequesturl' => new \moodle_url('https://lms.example.com/auth'), + 'jwksurl' => new \moodle_url('https://lms.example.com/jwks'), + 'accesstokenurl' => new \moodle_url('https://lms.example.com/token'), + 'id' => 24 + ], + 'deployment' => [ + 'name' => 'Site deployment', + 'deploymentid' => '' + ] + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'deploymentid' arg. Cannot be an empty string." + ] + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/context_test.php b/enrol/lti/tests/local/ltiadvantage/entity/context_test.php new file mode 100644 index 00000000000..b9f5d4d631b --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/context_test.php @@ -0,0 +1,206 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for context. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\context + */ +class context_test extends \advanced_testcase { + + /** + * Test creation of the object instances. + * + * @dataProvider instantiation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_creation(array $args, array $expectations) { + if (!$expectations['valid']) { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + context::create(...array_values($args)); + } else { + $context = context::create(...array_values($args)); + $this->assertEquals($args['deploymentid'], $context->get_deploymentid()); + $this->assertEquals($args['contextid'], $context->get_contextid()); + $this->assertEquals($args['types'], $context->get_types()); + $this->assertEquals($args['id'], $context->get_id()); + } + } + + /** + * Data provider for testing object instantiation. + * @return array[] the data for testing. + */ + public function instantiation_data_provider(): array { + return [ + 'Creation of a course section context' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a course offering context' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a course template context' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a course group context' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'http://purl.imsglobal.org/vocab/lis/v2/course#Group' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of an invalid context' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'http://example.com/invalid/context' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Cannot set invalid context type 'http://example.com/invalid/context'." + ] + ], + 'Creation of a simple name context with type CourseTemplate' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'CourseTemplate' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a simple name context with type CourseOffering' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'CourseOffering' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a simple name context with type CourseSection' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'CourseSection' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a simple name context with type Group' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'Group' + ], + 'id' => null + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a context with id' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'Group' + ], + 'id' => 24 + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Creation of a context with invalid id' => [ + 'args' => [ + 'deploymentid' => 24, + 'contextid' => 'context-123', + 'types' => [ + 'Group' + ], + 'id' => 0 + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "id must be a positive int" + ] + ], + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/deployment_test.php b/enrol/lti/tests/local/ltiadvantage/entity/deployment_test.php new file mode 100644 index 00000000000..81f252161c5 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/deployment_test.php @@ -0,0 +1,190 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for deployment. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\deployment + */ +class deployment_test extends \advanced_testcase { + + /** + * Test creation of the object instances. + * + * @dataProvider instantiation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_creation(array $args, array $expectations) { + if (!$expectations['valid']) { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + deployment::create(...array_values($args)); + } else { + $deployment = deployment::create(...array_values($args)); + $this->assertEquals($args['deploymentname'], $deployment->get_deploymentname()); + $this->assertEquals($args['deploymentid'], $deployment->get_deploymentid()); + $this->assertEquals($args['registrationid'], $deployment->get_registrationid()); + $this->assertEquals($args['legacyconsumerkey'], $deployment->get_legacy_consumer_key()); + $this->assertEquals($args['id'], $deployment->get_id()); + } + } + + /** + * Data provider for testing object instantiation. + * @return array the data for testing. + */ + public function instantiation_data_provider(): array { + return [ + 'Valid deployment creation, no id or legacy consumer key' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => 'deployment-abcde', + 'deploymentname' => 'Global platform deployment', + 'id' => null, + 'legacyconsumerkey' => null, + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Valid deployment creation, with id, no legacy consumer key' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => 'deployment-abcde', + 'deploymentname' => 'Global platform deployment', + 'id' => 45, + 'legacyconsumerkey' => null, + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Valid deployment creation, with id and legacy consumer key' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => 'deployment-abcde', + 'deploymentname' => 'Global platform deployment', + 'id' => 45, + 'legacyconsumerkey' => '1bcb23dfff', + ], + 'expectations' => [ + 'valid' => true, + ] + ], + 'Invalid deployment creation, invalid id' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => 'deployment-abcde', + 'deploymentname' => 'Global platform deployment', + 'id' => 0, + 'legacyconsumerkey' => null, + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'id must be a positive int' + ] + ], + 'Invalid deployment creation, empty deploymentname' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => 'deployment-abcde', + 'deploymentname' => '', + 'id' => null, + 'legacyconsumerkey' => null, + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'deploymentname' arg. Cannot be an empty string." + ] + ], + 'Invalid deployment creation, empty deploymentid' => [ + 'args' => [ + 'registrationid' => 99, + 'deploymentid' => '', + 'deploymentname' => 'Global platform deployment', + 'id' => null, + 'legacyconsumerkey' => null, + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'deploymentid' arg. Cannot be an empty string." + ] + ] + ]; + } + + /** + * Test verifying that a context can only be created from a deployment that has an id. + * + * @covers ::add_context + */ + public function test_add_context() { + $deploymentwithid = deployment::create(123, 'deploymentid123', 'Global tool deployment', 55); + $context = $deploymentwithid->add_context('context-id-123', ['CourseSection']); + $this->assertInstanceOf(context::class, $context); + $this->assertEquals(55, $context->get_deploymentid()); + + $deploymentwithoutid = deployment::create(123, 'deploymentid123', 'Global tool deployment'); + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage("Can't add context to a deployment that hasn't first been saved"); + $deploymentwithoutid->add_context('context-id-345', ['Group']); + } + + /** + * Test verifying that a resource_link can only be created from a deployment that has an id. + * + * @covers ::add_resource_link + */ + public function test_add_resource_link() { + $deploymentwithid = deployment::create(123, 'deploymentid123', 'Global tool deployment', 55); + $resourcelink = $deploymentwithid->add_resource_link('res-link-id-123', 45); + $this->assertInstanceOf(resource_link::class, $resourcelink); + $this->assertEquals(55, $resourcelink->get_deploymentid()); + + $resourcelink2 = $deploymentwithid->add_resource_link('res-link-id-456', 45, 66); + $this->assertEquals(66, $resourcelink2->get_contextid()); + + $deploymentwithoutid = deployment::create(123, 'deploymentid123', 'Global tool deployment'); + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage("Can't add resource_link to a deployment that hasn't first been saved"); + $deploymentwithoutid->add_resource_link('res-link-id-123', 45); + } + + /** + * Test the setter set_legacy_consumer_key. + * + * @covers ::set_legacy_consumer_key + */ + public function test_set_legacy_consumer_key() { + $deployment = deployment::create(12, 'deploy-id-123', 'Global tool deployment'); + $deployment->set_legacy_consumer_key(str_repeat('a', 255)); + $this->assertEquals(str_repeat('a', 255), $deployment->get_legacy_consumer_key()); + + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage('Legacy consumer key too long. Cannot exceed 255 chars.'); + $deployment->set_legacy_consumer_key(str_repeat('a', 256)); + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/migration_claim_test.php b/enrol/lti/tests/local/ltiadvantage/entity/migration_claim_test.php new file mode 100644 index 00000000000..6900408a513 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/migration_claim_test.php @@ -0,0 +1,193 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; + +/** + * Tests for migration_claim. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\migration_claim + */ +class migration_claim_test extends \advanced_testcase { + /** + * Setup run for each test case. + */ + protected function setUp(): void { + $this->resetAfterTest(); + } + + /** + * Returns a stub legacy_consumer_repository, allowing tests to verify claims using a predefined secret. + */ + protected function get_stub_legacy_consumer_repo() { + $mockedlegacyconsumerrepo = $this->createStub(legacy_consumer_repository::class); + $mockedlegacyconsumerrepo->method('get_consumer_secrets') + ->willReturn(['consumer_secret']); + return $mockedlegacyconsumerrepo; + } + + /** + * Test instantiation and getters of the migration_claim. + * + * @dataProvider migration_claim_provider + * @param array $migrationclaimdata the lti1p1 migration claim. + * @param string $deploymentid string id of the tool deployment. + * @param string $platform string url of the issuer. + * @param string $clientid string id of the client. + * @param string $exp expiry time. + * @param string $nonce nonce. + * @param legacy_consumer_repository $legacyconsumerrepo legacy consumer repo instance. + * @param array $expected array containing expectation data. + * @covers ::__construct + */ + public function test_migration_claim(array $migrationclaimdata, string $deploymentid, string $platform, + string $clientid, string $exp, string $nonce, legacy_consumer_repository $legacyconsumerrepo, + array $expected) { + + if (!empty($expected['exception'])) { + $this->expectException($expected['exception']); + $this->expectExceptionMessage($expected['exceptionmessage']); + new migration_claim($migrationclaimdata, $deploymentid, $platform, $clientid, $exp, $nonce, + $legacyconsumerrepo); + } else { + $migrationclaim = new migration_claim($migrationclaimdata, $deploymentid, $platform, $clientid, $exp, + $nonce, $legacyconsumerrepo); + $this->assertInstanceOf(migration_claim::class, $migrationclaim); + $this->assertEquals($expected['user_id'], $migrationclaim->get_user_id()); + $this->assertEquals($expected['context_id'], $migrationclaim->get_context_id()); + $this->assertEquals($expected['tool_consumer_instance_guid'], + $migrationclaim->get_tool_consumer_instance_guid()); + $this->assertEquals($expected['resource_link_id'], $migrationclaim->get_resource_link_id()); + } + } + + /** + * Data provider testing migration_claim instantiation. + * + * @return array[] the test case data. + */ + public function migration_claim_provider(): array { + // Note: See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim for details regarding the + // correct generation of oauth_consumer_key_sign signature. + return [ + 'Invalid - missing oauth_consumer_key' => [ + 'lti1p1migrationclaim' => [ + 'oauth_consumer_key' => '', + 'oauth_consumer_key_sign' => 'abcd', + ], + 'deploymentid' => 'D12345', + 'platform' => 'https://lms.example.org/', + 'clientid' => 'a1b2c3d4', + 'exp' => '1622612930', + 'nonce' => 'j45j2j5nnjn24544', + new legacy_consumer_repository(), + 'expected' => [ + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Missing 'oauth_consumer_key' property in lti1p1 migration claim." + ] + ], + 'Invalid - missing oauth_consumer_key_sign' => [ + 'lti1p1migrationclaim' => [ + 'oauth_consumer_key' => 'CONSUMER_1', + 'oauth_consumer_key_sign' => '', + ], + 'deploymentid' => 'D12345', + 'platform' => 'https://lms.example.org/', + 'clientid' => 'a1b2c3d4', + 'exp' => '1622612930', + 'nonce' => 'j45j2j5nnjn24544', + new legacy_consumer_repository(), + 'expected' => [ + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim." + ] + ], + 'Invalid - incorrect oauth_consumer_key_sign' => [ + 'lti1p1migrationclaim' => [ + 'oauth_consumer_key' => 'CONSUMER_1', + 'oauth_consumer_key_sign' => 'badsignature', + ], + 'deploymentid' => 'D12345', + 'platform' => 'https://lms.example.org/', + 'clientid' => 'a1b2c3d4', + 'exp' => '1622612930', + 'nonce' => 'j45j2j5nnjn24544', + new legacy_consumer_repository(), + 'expected' => [ + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim." + ] + ], + 'Valid - signature valid, map properties not provided' => [ + 'lti1p1migrationclaim' => [ + 'oauth_consumer_key' => 'CONSUMER_1', + 'oauth_consumer_key_sign' => base64_encode( + hash_hmac( + 'sha256', + 'CONSUMER_1&D12345&https://lms.example.org/&a1b2c3d4&1622612930&j45j2j5nnjn24544', + 'consumer_secret' + ) + ), + ], + 'deploymentid' => 'D12345', + 'platform' => 'https://lms.example.org/', + 'clientid' => 'a1b2c3d4', + 'exp' => '1622612930', + 'nonce' => 'j45j2j5nnjn24544', + $this->get_stub_legacy_consumer_repo(), + 'expected' => [ + 'user_id' => null, + 'context_id' => null, + 'tool_consumer_instance_guid' => null, + 'resource_link_id' => null + ] + ], + 'Valid - signature valid, map properties are provided' => [ + 'lti1p1migrationclaim' => [ + 'oauth_consumer_key' => 'CONSUMER_1', + 'oauth_consumer_key_sign' => base64_encode( + hash_hmac( + 'sha256', + 'CONSUMER_1&D12345&https://lms.example.org/&a1b2c3d4&1622612930&j45j2j5nnjn24544', + 'consumer_secret' + ) + ), + 'user_id' => '24', + 'context_id' => 'd345b', + 'tool_consumer_instance_guid' => '12345-123', + 'resource_link_id' => '4b6fa' + ], + 'deploymentid' => 'D12345', + 'platform' => 'https://lms.example.org/', + 'clientid' => 'a1b2c3d4', + 'exp' => '1622612930', + 'nonce' => 'j45j2j5nnjn24544', + $this->get_stub_legacy_consumer_repo(), + 'expected' => [ + 'user_id' => '24', + 'context_id' => 'd345b', + 'tool_consumer_instance_guid' => '12345-123', + 'resource_link_id' => '4b6fa' + ] + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/nrps_info_test.php b/enrol/lti/tests/local/ltiadvantage/entity/nrps_info_test.php new file mode 100644 index 00000000000..7e23fe90b90 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/nrps_info_test.php @@ -0,0 +1,101 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for nrps_info. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\nrps_info + */ +class nrps_info_test extends \advanced_testcase { + + /** + * Test creation of the object instances. + * + * @dataProvider instantiation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_create(array $args, array $expectations) { + if (!$expectations['valid']) { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + nrps_info::create(...array_values($args)); + } else { + $nrpsinfo = nrps_info::create(...array_values($args)); + $this->assertEquals($args['contextmembershipsurl'], $nrpsinfo->get_context_memberships_url()); + $this->assertEquals($expectations['serviceversions'], $nrpsinfo->get_service_versions()); + $this->assertEquals('https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', + $nrpsinfo->get_service_scope()); + } + } + + /** + * Data provider for testing object instantiation. + * @return array the data for testing. + */ + public function instantiation_data_provider(): array { + return [ + 'Valid creation' => [ + 'args' => [ + 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), + 'serviceversions' => ['1.0', '2.0'], + ], + 'expectations' => [ + 'valid' => true, + 'serviceversions' => ['1.0', '2.0'] + ] + ], + 'Missing service version' => [ + 'args' => [ + 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), + 'serviceversions' => [], + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Service versions array cannot be empty' + ] + ], + 'Invalid service version' => [ + 'args' => [ + 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), + 'serviceversions' => ['1.1'], + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid Names and Roles service version '1.1'" + ] + ], + 'Duplicate service version' => [ + 'args' => [ + 'contextmembershipsurl' => new \moodle_url('https://lms.example.com/45/memberships'), + 'serviceversions' => ['1.0', '1.0'], + ], + 'expectations' => [ + 'valid' => true, + 'serviceversions' => ['1.0'] + ] + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/registration_url_test.php b/enrol/lti/tests/local/ltiadvantage/entity/registration_url_test.php new file mode 100644 index 00000000000..037feaa3e8a --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/registration_url_test.php @@ -0,0 +1,101 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for registration_url. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\registration_url + */ +class registration_url_test extends \advanced_testcase { + + /** + * Test the creation and validation of a registration_url instance. + * + * @dataProvider registration_url_data_provider + * @param array $args the arguments to the constructor. + * @param array $expectations various expectations for the test cases. + * @covers ::__construct + */ + public function test_registration_url(array $args, array $expectations) { + if ($expectations['valid']) { + $regurl = new registration_url(...array_values($args)); + $this->assertEquals($expectations['expirytime'], $regurl->get_expiry_time()); + if (!empty($expectations['token'])) { + $this->assertEquals($expectations['token'], $regurl->param('token')); + } else { + $this->assertNotEmpty($regurl->param('token')); + } + } else { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + new registration_url(...array_values($args)); + } + } + + /** + * Data provider used to test registration_url object creation. + * + * @return array the array of test data. + */ + public function registration_url_data_provider(): array { + return [ + 'Valid, required args only, expiry 0' => [ + 'args' => [ + 'expirytime' => 0 + ], + 'expectations' => [ + 'valid' => true, + 'expirytime' => 0, + ] + ], + 'Valid, required args only, expiry positive' => [ + 'args' => [ + 'expirytime' => 50 + ], + 'expectations' => [ + 'valid' => true, + 'expirytime' => 50, + ] + ], + 'Invalid, required args only, expiry negative' => [ + 'args' => [ + 'expirytime' => -70 + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Invalid registration_url expiry time. Must be greater than or equal to 0.' + ] + ], + 'Valid, all args provided' => [ + 'args' => [ + 'expirytime' => 56, + 'token' => 'token-abcde-12345' + ], + 'expectations' => [ + 'valid' => true, + 'expirytime' => 56, + 'token' => 'token-abcde-12345' + ] + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/resource_link_test.php b/enrol/lti/tests/local/ltiadvantage/entity/resource_link_test.php new file mode 100644 index 00000000000..887720d5698 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/resource_link_test.php @@ -0,0 +1,199 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for resource_link. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\resource_link + */ +class resource_link_test extends \advanced_testcase { + /** + * Test creation of the object instances. + * + * @dataProvider instantiation_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_create(array $args, array $expectations) { + if (!$expectations['valid']) { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + resource_link::create(...array_values($args)); + } else { + $reslink = resource_link::create(...array_values($args)); + $this->assertEquals($args['resourcelinkid'], $reslink->get_resourcelinkid()); + $this->assertEquals($args['resourceid'], $reslink->get_resourceid()); + $this->assertEquals($args['deploymentid'], $reslink->get_deploymentid()); + $this->assertEquals($args['contextid'], $reslink->get_contextid()); + $this->assertEquals($args['id'], $reslink->get_id()); + $this->assertEquals(null, $reslink->get_grade_service()); + $this->assertEquals(null, $reslink->get_names_and_roles_service()); + } + } + + /** + * Data provider for testing object instantiation. + * @return array the data for testing. + */ + public function instantiation_data_provider(): array { + return [ + 'Valid creation, no context or id provided' => [ + 'args' => [ + 'resourcelinkid' => 'res-link-id-123', + 'deploymentid' => 45, + 'resourceid' => 24, + 'contextid' => null, + 'id' => null + ], + 'expectations' => [ + 'valid' => true + ] + + ], + 'Valid creation, context id provided, no id provided' => [ + 'args' => [ + 'resourcelinkid' => 'res-link-id-123', + 'deploymentid' => 45, + 'resourceid' => 24, + 'contextid' => 777, + 'id' => null + ], + 'expectations' => [ + 'valid' => true + ] + + ], + 'Valid creation, context id and id provided' => [ + 'args' => [ + 'resourcelinkid' => 'res-link-id-123', + 'deploymentid' => 45, + 'resourceid' => 24, + 'contextid' => 777, + 'id' => 33 + ], + 'expectations' => [ + 'valid' => true + ] + ], + 'Invlid creation, empty resource link id string' => [ + 'args' => [ + 'resourcelinkid' => '', + 'deploymentid' => 45, + 'resourceid' => 24, + 'contextid' => null, + 'id' => null + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Error: resourcelinkid cannot be an empty string' + ] + + ] + ]; + } + + /** + * Test confirming that a grade service instance can be added to the object instance. + * + * @covers ::add_grade_service + */ + public function test_add_grade_service() { + $reslink = resource_link::create('res-link-id-123', 24, 44); + $this->assertNull($reslink->get_grade_service()); + $reslink->add_grade_service( + new \moodle_url('https://platform.example.org/10/lineitems'), + new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'] + ); + $gradeservice = $reslink->get_grade_service(); + $this->assertInstanceOf(ags_info::class, $gradeservice); + $this->assertEquals(new \moodle_url('https://platform.example.org/10/lineitems'), + $gradeservice->get_lineitemsurl()); + $this->assertEquals(new \moodle_url('https://platform.example.org/10/lineitems/4/lineitem'), + $gradeservice->get_lineitemurl()); + $this->assertEquals(['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'], $gradeservice->get_scopes()); + } + + /** + * Test confirming that a names and roles service instance can be added to the object instance. + * + * @covers ::add_names_and_roles_service + */ + public function test_add_names_and_roles_service() { + $reslink = resource_link::create('res-link-id-123', 24, 44); + $this->assertNull($reslink->get_names_and_roles_service()); + $reslink->add_names_and_roles_service(new \moodle_url('https://lms.example.com/10/memberships'), ['2.0']); + $nrps = $reslink->get_names_and_roles_service(); + $this->assertInstanceOf(nrps_info::class, $nrps); + $this->assertEquals(new \moodle_url('https://lms.example.com/10/memberships'), + $nrps->get_context_memberships_url()); + $this->assertEquals(['2.0'], $nrps->get_service_versions()); + } + + /** + * Verify that a user can be created from a resource link that has an id. + * + * @covers ::add_user + */ + public function test_add_user() { + $reslinkwithid = resource_link::create('res-link-id-123', 24, 44, 66, 33); + $user = $reslinkwithid->add_user(2, 'platform-user-id-123', 'en', 'Sydney', 'AU', 'Test university', '99'); + $this->assertInstanceOf(user::class, $user); + $this->assertEquals(33, $user->get_resourcelinkid()); + + $reslinkwithoutid = resource_link::create('res-link-id-123', 24, 44); + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage("Can't add user to a resource_link that hasn't first been saved"); + $reslinkwithoutid->add_user(2, 'platform-user-id-123', 'en', 'Sydney', 'Australia', 'Test university', '99'); + } + + /** + * Test confirming that the resourceid can be changed on the object. + * + * @covers ::set_resourceid + */ + public function test_set_resource_id() { + $reslink = resource_link::create('res-link-id-123', 24, 44); + $this->assertEquals(44, $reslink->get_resourceid()); + $reslink->set_resourceid(333); + $this->assertEquals(333, $reslink->get_resourceid()); + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage('Resource id must be a positive int'); + $reslink->set_resourceid(0); + } + + /** + * Test confirming that the contextid can be changed on the object. + * + * @covers ::set_contextid + */ + public function test_set_context_id() { + $reslink = resource_link::create('res-link-id-123', 24, 44); + $this->assertEquals(null, $reslink->get_contextid()); + $reslink->set_contextid(333); + $this->assertEquals(333, $reslink->get_contextid()); + $this->expectException(\coding_exception::class); + $this->expectExceptionMessage('Context id must be a positive int'); + $reslink->set_contextid(0); + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/entity/user_test.php b/enrol/lti/tests/local/ltiadvantage/entity/user_test.php new file mode 100644 index 00000000000..e456961fda2 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/entity/user_test.php @@ -0,0 +1,707 @@ +. + +namespace enrol_lti\local\ltiadvantage\entity; + +/** + * Tests for user. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\entity\user + */ +class user_test extends \advanced_testcase { + + /** + * Test creation of a user instance using the factory method. + * + * @dataProvider create_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create + */ + public function test_create(array $args, array $expectations) { + if ($expectations['valid']) { + $user = user::create(...array_values($args)); + $this->assertInstanceOf(user::class, $user); + $this->assertEquals($expectations['id'], $user->get_id()); + $this->assertEquals($expectations['localid'], $user->get_localid()); + $this->assertEquals($expectations['resourcelinkid'], $user->get_resourcelinkid()); + $this->assertEquals($expectations['resourceid'], $user->get_resourceid()); + $this->assertEquals($expectations['deploymentid'], $user->get_deploymentid()); + $this->assertEquals($expectations['sourceid'], $user->get_sourceid()); + $this->assertEquals($expectations['lang'], $user->get_lang()); + $this->assertEquals($expectations['timezone'], $user->get_timezone()); + $this->assertEquals($expectations['city'], $user->get_city()); + $this->assertEquals($expectations['country'], $user->get_country()); + $this->assertEquals($expectations['institution'], $user->get_institution()); + $this->assertEquals($expectations['maildisplay'], $user->get_maildisplay()); + $this->assertEquals($expectations['lastgrade'], $user->get_lastgrade()); + $this->assertEquals($expectations['lastaccess'], $user->get_lastaccess()); + } else { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + user::create(...array_values($args)); + } + } + + /** + * Data provider for testing the user::create() method. + * + * @return array the data for testing. + */ + public function create_data_provider(): array { + return [ + 'Valid create, only required args provided' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99' + ], + 'expectations' => [ + 'valid' => true, + 'resourceid' => 22, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => '', + 'country' => '', + 'institution' => '', + 'maildisplay' => 2, + 'lastgrade' => 0.0, + 'lastaccess' => null, + 'id' => null, + 'localid' => 2, + 'resourcelinkid' => null, + ] + ], + 'Valid create, all args provided explicitly' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'My institution', + 'maildisplay' => 1, + 'lastgrade' => 50.55, + 'lastaccess' => 14567888, + 'resourcelinkid' => 44, + 'id' => 22 + ], + 'expectations' => [ + 'valid' => true, + 'resourceid' => 22, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'My institution', + 'maildisplay' => 1, + 'lastgrade' => 50.55, + 'lastaccess' => 14567888, + 'resourcelinkid' => 44, + 'localid' => 2, + 'id' => 22, + ] + ], + 'Valid create, optional args explicitly nulled for default values' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'My institution', + 'maildisplay' => null, + 'lastgrade' => null, + 'lastaccess' => null, + 'resourcelinkid' => null, + 'id' => null + + ], + 'expectations' => [ + 'valid' => true, + 'resourceid' => 22, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'My institution', + 'maildisplay' => 2, + 'lastgrade' => 0.0, + 'lastaccess' => null, + 'resourcelinkid' => null, + 'localid' => 2, + 'id' => null + ] + ], + 'Invalid create, lang with bad value (fr not installed)' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'fr', + 'timezone' => '99', + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid lang 'fr' provided." + ] + ], + 'Invalid create, timezone with bad value' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => 'NOT/FOUND', + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." + ] + ], + 'Invalid create, explicitly provided country with bad value' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => '', + 'country' => 'FFF', + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid country code 'FFF'." + ] + ], + 'Invalid create, explicit maildisplay with bad value' => [ + 'args' => [ + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => '', + 'country' => '', + 'institution' => '', + 'maildisplay' => 3, + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid maildisplay value '3'. Must be in the range {0..2}." + ] + ], + ]; + } + + /** + * Test creation of a user instance from a resource link. + * + * @dataProvider create_from_resource_link_data_provider + * @param array $args the arguments to the creation method. + * @param array $expectations various expectations for the test cases. + * @covers ::create_from_resource_link + */ + public function test_create_from_resource_link(array $args, array $expectations) { + if ($expectations['valid']) { + $user = user::create_from_resource_link(...array_values($args)); + $this->assertInstanceOf(user::class, $user); + $this->assertEquals($expectations['id'], $user->get_id()); + $this->assertEquals($expectations['localid'], $user->get_localid()); + $this->assertEquals($expectations['resourcelinkid'], $user->get_resourcelinkid()); + $this->assertEquals($expectations['resourceid'], $user->get_resourceid()); + $this->assertEquals($expectations['deploymentid'], $user->get_deploymentid()); + $this->assertEquals($expectations['sourceid'], $user->get_sourceid()); + $this->assertEquals($expectations['lang'], $user->get_lang()); + $this->assertEquals($expectations['city'], $user->get_city()); + $this->assertEquals($expectations['country'], $user->get_country()); + $this->assertEquals($expectations['institution'], $user->get_institution()); + $this->assertEquals($expectations['timezone'], $user->get_timezone()); + $this->assertEquals($expectations['maildisplay'], $user->get_maildisplay()); + $this->assertEquals($expectations['lastgrade'], $user->get_lastgrade()); + $this->assertEquals($expectations['lastaccess'], $user->get_lastaccess()); + } else { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + user::create_from_resource_link(...array_values($args)); + } + } + + /** + * Data provider used in testing the user::create_from_resource_link() method. + * + * @return array the data for testing. + */ + public function create_from_resource_link_data_provider(): array { + return [ + 'Valid creation, all args provided explicitly' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'platform', + 'maildisplay' => 1 + ], + 'expectations' => [ + 'valid' => true, + 'id' => null, + 'localid' => 2, + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'platform', + 'maildisplay' => 1, + 'lastgrade' => 0.0, + 'lastaccess' => null + ] + ], + 'Valid creation, only required args provided, explicit values' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => 'UTC' + ], + 'expectations' => [ + 'valid' => true, + 'id' => null, + 'localid' => 2, + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => 'UTC', + 'city' => '', + 'country' => '', + 'institution' => '', + 'maildisplay' => 2, + 'lastgrade' => 0.0, + 'lastaccess' => null + ] + ], + 'Invalid creation, only required args provided, empty sourceid' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'user' => 2, + 'deploymentid' => 33, + 'sourceid' => '', + 'lang' => 'en', + 'timezone' => 'UTC' + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Invalid sourceid value. Cannot be an empty string.' + ] + ], + 'Invalid creation, only required args provided, empty lang' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'user' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => '', + 'timezone' => 'UTC' + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Invalid lang value. Cannot be an empty string.' + ] + ], + 'Invalid creation, only required args provided, empty timezone' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '' + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Invalid timezone value. Cannot be an empty string.' + ] + ], + 'Invalid creation, only required args provided, invalid lang (fr not installed)' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'fr', + 'timezone' => 'UTC' + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid lang 'fr' provided." + ] + ], + 'Invalid creation, only required args provided, invalid timezone' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => 'NOT/FOUND' + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." + ] + ], + 'Invalid creation, all args provided explicitly, invalid country' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'FFF', + 'institution' => 'platform', + 'maildisplay' => 1 + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid country code 'FFF'." + ] + ], + 'Invalid creation, all args provided explicitly, invalid maildisplay' => [ + 'args' => [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => '99', + 'city' => 'Melbourne', + 'country' => 'AU', + 'institution' => 'platform', + 'maildisplay' => 4 + ], + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid maildisplay value '4'. Must be in the range {0..2}." + ] + ], + ]; + } + + /** + * Helper to create a simple, working user for testing. + * + * @return user a user instance. + */ + protected function create_test_user(): user { + $args = [ + 'resourcelinkid' => 11, + 'resourceid' => 22, + 'userid' => 2, + 'deploymentid' => 33, + 'sourceid' => 'user-id-123', + 'lang' => 'en', + 'timezone' => 'UTC' + ]; + return user::create_from_resource_link(...array_values($args)); + } + + + /** + * Test the behaviour of the user setters and getters. + * + * @dataProvider setters_getters_data_provider + * @param string $methodname the name of the setter + * @param mixed $arg the argument to the setter + * @param array $expectations the array of expectations + * @covers ::__construct + */ + public function test_setters_and_getters(string $methodname, $arg, array $expectations) { + $user = $this->create_test_user(); + $setter = 'set_'.$methodname; + $getter = 'get_'.$methodname; + if ($expectations['valid']) { + $user->$setter($arg); + if (isset($expectations['expectedvalue'])) { + $this->assertEquals($expectations['expectedvalue'], $user->$getter()); + } else { + $this->assertEquals($arg, $user->$getter()); + } + + } else { + $this->expectException($expectations['exception']); + $this->expectExceptionMessage($expectations['exceptionmessage']); + $user->$setter($arg); + } + } + + /** + * Data provider for testing the user object setters. + * + * @return array the array of test data. + */ + public function setters_getters_data_provider(): array { + return [ + 'Testing set_resourcelinkid with valid id' => [ + 'methodname' => 'resourcelinkid', + 'arg' => 8, + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_resourcelinkid with invalid id' => [ + 'methodname' => 'resourcelinkid', + 'arg' => -1, + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid resourcelinkid '-1' provided. Must be > 0." + ] + ], + 'Testing set_city with a non-empty string' => [ + 'methodname' => 'city', + 'arg' => 'Melbourne', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_city with an empty string' => [ + 'methodname' => 'city', + 'arg' => '', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_country with a valid country code' => [ + 'methodname' => 'country', + 'arg' => 'AU', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_country with an empty string' => [ + 'methodname' => 'country', + 'arg' => '', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_country with an invalid country code' => [ + 'methodname' => 'country', + 'arg' => 'FFF', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid country code 'FFF'." + ] + ], + 'Testing set_institution with a non-empty string' => [ + 'methodname' => 'institution', + 'arg' => 'Some institution', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_institution with an empty string' => [ + 'methodname' => 'institution', + 'arg' => '', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_timezone with a valid real timezone' => [ + 'methodname' => 'timezone', + 'arg' => 'Pacific/Wallis', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_timezone with a valid server timezone value' => [ + 'methodname' => 'timezone', + 'arg' => '99', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_timezone with an invalid timezone value' => [ + 'methodname' => 'timezone', + 'arg' => 'NOT/FOUND', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid timezone 'NOT/FOUND' provided." + ] + ], + 'Testing set_maildisplay with a valid int: 0' => [ + 'methodname' => 'maildisplay', + 'arg' => '0', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_maildisplay with a valid int: 1' => [ + 'methodname' => 'maildisplay', + 'arg' => '1', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_maildisplay with a valid int: 2' => [ + 'methodname' => 'maildisplay', + 'arg' => '1', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_maildisplay with invalid int' => [ + 'methodname' => 'maildisplay', + 'arg' => '-1', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid maildisplay value '-1'. Must be in the range {0..2}." + ] + ], + 'Testing set_maildisplay with invalid int' => [ + 'methodname' => 'maildisplay', + 'arg' => '3', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid maildisplay value '3'. Must be in the range {0..2}." + ] + ], + 'Testing set_lang with valid lang code' => [ + 'methodname' => 'lang', + 'arg' => 'en', + 'expectations' => [ + 'valid' => true, + ] + ], + 'Testing set_lang with an empty string' => [ + 'methodname' => 'lang', + 'arg' => '', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Invalid lang value. Cannot be an empty string.' + ] + ], + 'Testing set_lang with an empty string' => [ + 'methodname' => 'lang', + 'arg' => 'ff', + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => "Invalid lang 'ff' provided." + ] + ], + 'Testing set_lastgrade with valid grade' => [ + 'methodname' => 'lastgrade', + 'arg' => 0.0, + 'expectations' => [ + 'valid' => true + ] + ], + 'Testing set_lastgrade with valid non zero grade' => [ + 'methodname' => 'lastgrade', + 'arg' => 150.0, + 'expectations' => [ + 'valid' => true + ] + ], + 'Testing set_lastgrade with valid non zero long decimal grade' => [ + 'methodname' => 'lastgrade', + 'arg' => 150.777779, + 'expectations' => [ + 'valid' => true, + 'expectedvalue' => 150.77778 + ] + ], + 'Testing set_lastaccess with valid time' => [ + 'methodname' => 'lastaccess', + 'arg' => 4, + 'expectations' => [ + 'valid' => true + ] + ], + 'Testing set_lastaccess with invalid time' => [ + 'methodname' => 'lastaccess', + 'arg' => -1, + 'expectations' => [ + 'valid' => false, + 'exception' => \coding_exception::class, + 'exceptionmessage' => 'Cannot set negative access time' + ] + ], + ]; + } +}