diff --git a/enrol/lti/classes/local/ltiadvantage/lib/http_client.php b/enrol/lti/classes/local/ltiadvantage/lib/http_client.php new file mode 100644 index 00000000000..2762d94abbc --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/lib/http_client.php @@ -0,0 +1,102 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use Packback\Lti1p3\Interfaces\IHttpClient; +use Packback\Lti1p3\Interfaces\IHttpException; +use Packback\Lti1p3\Interfaces\IHttpResponse; + +/** + * An implementation of IHTTPClient delegating to a curl object, for use with the lib/lti1p3 library code. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class http_client implements IHttpClient { + + /** @var \curl a curl client instance. */ + private $curlclient; + + /** + * Constructor. + * + * @param \curl $curlclient a curl client instance. + */ + public function __construct(\curl $curlclient) { + $this->curlclient = $curlclient; + } + + /** + * Make an HTTP request to the given URL. + * + * @param string $method the HTTP method to use. + * @param string $url the URL to send the request to. + * @param array $options an array of request options, mainly used to set headers and body. + * @return IHttpResponse the response + * @throws \Exception if the curl client encounters any errors making the request. + * @throws IHttpException if the response contains a 400-level or 500-level status code. + */ + public function request(string $method, string $url, array $options): IHttpResponse { + $this->curlclient->resetHeader(); + $this->curlclient->resetopt(); + if (isset($options['headers'])) { + $headers = $options['headers']; + array_walk( + $headers, + function(&$val, $key) { + $val = "$key: $val"; + } + ); + $this->curlclient->setHeader($headers); + } + if ($method == 'POST') { + $res = $this->curlclient->post($url, $options['body'] ?? null, ['CURLOPT_HEADER' => 1]); + } else if ($method == 'GET') { + $res = $this->curlclient->get($url, [], ['CURLOPT_HEADER' => 1]); + } else { + throw new \Exception('Sorry, that HTTP method is not supported yet.'); + } + + $info = $this->curlclient->get_info(); + if (!$this->curlclient->get_errno() && !$this->curlclient->error) { + // No errors, so format the response. + $headersize = $info['header_size']; + $resheaders = substr($res, 0, $headersize); + $resbody = substr($res, $headersize); + $headerlines = array_filter(explode("\r\n", $resheaders)); + $parsedresponseheaders = [ + 'httpstatus' => array_shift($headerlines) + ]; + foreach ($headerlines as $headerline) { + $headerbits = explode(':', $headerline, 2); + if (count($headerbits) == 2) { + // Only parse headers having colon separation. + $parsedresponseheaders[$headerbits[0]] = $headerbits[1]; + } + } + $response = new http_response(['headers' => $parsedresponseheaders, 'body' => $resbody], intval($info['http_code'])); + if ($response->getStatusCode() >= 400) { + throw new http_exception($response, "An HTTP error status was received: '{$response->getHeaders()['httpstatus']}'"); + } + return $response; + } + // The curl client experienced errors, so report that. + throw new \Exception("There was a cURL error when making the request: errno: {$this->curlclient->get_errno()}, + error: {$this->curlclient->error}."); + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/lib/http_exception.php b/enrol/lti/classes/local/ltiadvantage/lib/http_exception.php new file mode 100644 index 00000000000..eb0644e6f7c --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/lib/http_exception.php @@ -0,0 +1,56 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use Packback\Lti1p3\Interfaces\IHttpException; +use Packback\Lti1p3\Interfaces\IHttpResponse; + +/** + * An implementation of IHTTPException, for use with the lib/lti1p3 library code. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class http_exception extends \Exception implements IHttpException { + + /** @var IHttpResponse the response to which this exception relates.*/ + protected $response; + + /** + * Constructor. + * + * @param IHttpResponse $response a response instance. + * @param string $message + * @param int $code + * @param \Throwable|null $previous + */ + public function __construct(IHttpResponse $response, $message = "", $code = 0, \Throwable $previous = null) { + + parent::__construct($message, $code, $previous); + $this->response = $response; + } + + /** + * Get the http response. + * + * @return IHttpResponse the response. + */ + public function getResponse(): IHttpResponse { + return $this->response; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/lib/http_response.php b/enrol/lti/classes/local/ltiadvantage/lib/http_response.php new file mode 100644 index 00000000000..3ebb219be4f --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/lib/http_response.php @@ -0,0 +1,86 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use Packback\Lti1p3\Interfaces\IHttpResponse; + +/** + * An implementation of IHTTPResponse, for use with the lib/lti1p3 library code. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class http_response implements IHttpResponse { + + /** @var string HTTP response body */ + private $body; + + /** @var array HTTP response header lines */ + private $headers; + + /** @var int http status code */ + private $statuscode; + + /** + * Constructor. + * + * @param array $payload the array containing the body and headers. + * @param int $statuscode the HTTP status code. + */ + public function __construct(array $payload, int $statuscode) { + $this->parse_payload($payload); + $this->statuscode = $statuscode; + } + + /** + * Parse the array containing headers and body into instance vars. + * + * @param array $payload + */ + private function parse_payload(array $payload): void { + $this->headers = $payload['headers']; + $this->body = $payload['body']; + } + + /** + * Get the HTTP response body string. + * + * @return string the HTTP response body. + */ + public function getBody() { + return $this->body; + } + + /** + * Get the HTTP headers array. + * + * @return array the array containing the headers. + */ + public function getHeaders(): array { + return $this->headers; + } + + /** + * Get the HTTP response status code. + * + * @return int the HTTP response status code. + */ + public function getStatusCode(): int { + return $this->statuscode; + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/lib/issuer_database.php b/enrol/lti/classes/local/ltiadvantage/lib/issuer_database.php new file mode 100644 index 00000000000..1833c6773d4 --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/lib/issuer_database.php @@ -0,0 +1,110 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; +use enrol_lti\local\ltiadvantage\repository\application_registration_repository; +use enrol_lti\local\ltiadvantage\repository\deployment_repository; +use Packback\Lti1p3\Interfaces\IDatabase; +use Packback\Lti1p3\LtiDeployment; +use Packback\Lti1p3\LtiRegistration; + +/** + * The issuer_database class, providing a read-only store of issuer details. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issuer_database implements IDatabase { + + /** @var application_registration_repository an application registration repository instance used for lookups.*/ + private $appregrepo; + + /** @var deployment_repository a deployment repository instance for lookups.*/ + private $deploymentrepo; + + /** + * The issuer_database constructor. + * @param application_registration_repository $appregrepo an application registration repository instance. + * @param deployment_repository $deploymentrepo a deployment repository instance. + */ + public function __construct(application_registration_repository $appregrepo, + deployment_repository $deploymentrepo) { + + $this->appregrepo = $appregrepo; + $this->deploymentrepo = $deploymentrepo; + } + + /** + * Find and return an LTI registration based on its unique {issuer, client_id} tuple. + * + * @param string $iss the issuer id. + * @param string $clientId the client_id of the registration. + * @return LtiRegistration|null The registration object, or null if not found. + */ + public function findRegistrationByIssuer($iss, $clientId = null): ?LtiRegistration { + if (is_null($clientId)) { + throw new \coding_exception("Both issuer and client id are required to identify platform registrations ". + "and must be sent by your LMS during both login requests (as 'client_id') and in the auth response ". + "Tool JWT (as the 'aud' claim)."); + } + + global $CFG; + require_once($CFG->libdir . '/moodlelib.php'); // For get_config() usage. + $reg = $this->appregrepo->find_by_platform($iss, $clientId); + if (!$reg) { + return null; + } + $privatekey = get_config('enrol_lti', 'lti_13_privatekey'); + $kid = get_config('enrol_lti', 'lti_13_kid'); + + return LtiRegistration::new() + ->setAuthLoginUrl($reg->get_authenticationrequesturl()->out(false)) + ->setAuthTokenUrl($reg->get_accesstokenurl()->out(false)) + ->setClientId($reg->get_clientid()) + ->setKeySetUrl($reg->get_jwksurl()->out(false)) + ->setKid($kid) + ->setIssuer($reg->get_platformid()->out(false)) + ->setToolPrivateKey($privatekey); + } + + /** + * Returns an LTI deployment based on the {issuer, client_id} tuple and a deployment id string. + * + * @param string $iss the issuer id. + * @param string $deploymentId the deployment id. + * @param string $clientId the client_id of the registration. + * @return LtiDeployment|null The deployment object or null if not found. + */ + public function findDeployment($iss, $deploymentId, $clientId = null): ?LtiDeployment { + if (is_null($clientId)) { + throw new \coding_exception("Both issuer and client id are required to identify platform registrations ". + "and must be sent by your LMS during both login requests (as 'client_id') and in the auth response ". + "Tool JWT (as the 'aud' claim)."); + } + + $appregistration = $this->appregrepo->find_by_platform($iss, $clientId); + if (!$appregistration) { + return null; + } + $deployment = $this->deploymentrepo->find_by_registration($appregistration->get_id(), $deploymentId); + if (!$deployment) { + return null; + } + return LtiDeployment::new() + ->setDeploymentId($deployment->get_deploymentid()); + } +} diff --git a/enrol/lti/classes/local/ltiadvantage/lib/launch_cache_session.php b/enrol/lti/classes/local/ltiadvantage/lib/launch_cache_session.php new file mode 100644 index 00000000000..38df8d9085f --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/lib/launch_cache_session.php @@ -0,0 +1,120 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use Packback\Lti1p3\Interfaces\ICache; + +/** + * The launch_cache_session, providing a temporary session store for launch information. + * + * This is used to store the launch information while the user is transitioned through the Moodle authentication flows + * and back to the deep linking launch handler (launch_deeplink.php). + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class launch_cache_session implements ICache { + + /** + * Get the launch data from the cache. + * + * @param string $key the launch id. + * @return array|null the launch data. + */ + public function getLaunchData($key): ?array { + global $SESSION; + if (isset($SESSION->enrol_lti_launch[$key])) { + return unserialize($SESSION->enrol_lti_launch[$key]); + } + return null; + } + + /** + * Add launch data to the cache. + * + * @param string $key the launch id. + * @param array $jwtBody the launch data. + */ + public function cacheLaunchData(string $key, array $jwtBody): void { + global $SESSION; + $SESSION->enrol_lti_launch[$key] = serialize($jwtBody); + } + + /** + * Cache the nonce. + * + * @param string $nonce the nonce. + * @param string $state the state. + */ + public function cacheNonce(string $nonce, string $state): void { + global $SESSION; + $SESSION->enrol_lti_launch_nonce[$nonce] = $state; + } + + /** + * Check whether the cache contains the nonce. + * + * @param string $nonce the nonce + * @param string $state the state + * @return bool true if found, false otherwise. + */ + public function checkNonceIsValid(string $nonce, string $state): bool { + global $SESSION; + return isset($SESSION->enrol_lti_launch_nonce[$nonce]) && $SESSION->enrol_lti_launch_nonce[$nonce] == $state; + } + + /** + * Delete all data from the session cache. + */ + public function purge() { + global $SESSION; + unset($SESSION->enrol_lti_launch); + } + + /** + * Cache the access token. + * + * @param string $key the key + * @param string $accessToken the access token + */ + public function cacheAccessToken(string $key, string $accessToken): void { + global $SESSION; + $SESSION->enrol_lti_launch_token[$key] = $accessToken; + } + + /** + * Get a cached access token. + * + * @param string $key the key to check. + * @return string|null the token string, or null if not found. + */ + public function getAccessToken(string $key): ?string { + global $SESSION; + return $SESSION->enrol_lti_launch_token[$key] ?? null; + } + + /** + * Clear the access token from the cache. + * + * @param string $key the key to purge. + */ + public function clearAccessToken(string $key): void { + global $SESSION; + unset($SESSION->enrol_lti_launch_token[$key]); + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/lib/http_client_test.php b/enrol/lti/tests/local/ltiadvantage/lib/http_client_test.php new file mode 100644 index 00000000000..392f57d534e --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/lib/http_client_test.php @@ -0,0 +1,119 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +/** + * Tests for the http_client class. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\lib\http_client + */ +class http_client_test extends \basic_testcase { + + /** + * Verify the http_client delegates to curl during a "GET" request. + * + * @covers ::request + */ + public function test_client_get_request() { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + $mockcurl = $this->createMock(\curl::class); + $mockcurl->expects($this->once()) + ->method('get') + ->with( + $this->equalTo('https://example.com'), + $this->equalTo([]), + $this->equalTo(['CURLOPT_HEADER' => 1]) + ); + $mockcurl->expects($this->any()) + ->method('get_info') + ->willReturnCallback(function() { + return ['header_size' => 0, 'http_code' => 200]; + }); + $mockcurl->expects($this->once()) + ->method('setHeader') + ->with($this->equalTo(['someheader' => 'someheader: headervalue'])); + + $client = new http_client($mockcurl); + $client->request('GET', 'https://example.com', ['headers' => ['someheader' => 'headervalue']]); + } + + /** + * Verify the http_client delegates to curl during a "POST" request. + * + * @covers ::request + */ + public function test_client_post_request() { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + $mockcurl = $this->createMock(\curl::class); + $mockcurl->expects($this->once()) + ->method('post') + ->with( + $this->equalTo('https://example.com'), + $this->equalTo('examplebody'), + $this->equalTo(['CURLOPT_HEADER' => 1]) + ); + $mockcurl->expects($this->any()) + ->method('get_info') + ->willReturnCallback(function() { + return ['header_size' => 0, 'http_code' => 200]; + }); + $mockcurl->expects($this->once()) + ->method('setHeader') + ->with($this->equalTo(['someheader' => 'someheader: headervalue'])); + + $client = new http_client($mockcurl); + $client->request('POST', 'https://example.com', ['headers' => ['someheader' => 'headervalue'], 'body' => 'examplebody']); + } + + /** + * Test a few of the unsupported HTTP methods. + * + * @dataProvider unsupported_methods_provider + * @param string $httpmethod the http method. + * @covers ::request + */ + public function test_request_unsupported_method(string $httpmethod) { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + $mockcurl = $this->createMock(\curl::class); + + $client = new http_client($mockcurl); + $this->expectException(\Exception::class); + $client->request($httpmethod, 'https://example.com', []); + } + + /** + * Data provider for testing unsupported http methods. + * + * @return array the test case data. + */ + public function unsupported_methods_provider() { + return [ + 'head' => ['HEAD'], + 'put' => ['PUT'], + 'delete' => ['DELETE'], + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/lib/http_exception_test.php b/enrol/lti/tests/local/ltiadvantage/lib/http_exception_test.php new file mode 100644 index 00000000000..ae3684f5764 --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/lib/http_exception_test.php @@ -0,0 +1,100 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use Packback\Lti1p3\Interfaces\IHttpResponse; + +/** + * Tests for the http_exception class. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\lib\http_exception + */ +class http_exception_test extends \basic_testcase { + + /** + * Test constructor and getters for a range of inputs. + * + * @dataProvider exception_data_provider + * @param array $args the arguments to the exception constructor. + * @covers ::__construct + */ + public function test_exception(array $args) { + + $exception = new http_exception(...array_values($args)); + $this->assertInstanceOf(IHttpResponse::class, $exception->getResponse()); + if (isset($args['message'])) { + $this->assertEquals($args['message'], $exception->getMessage()); + } + if (isset($args['code'])) { + $this->assertEquals($args['code'], $exception->getCode()); + } + if (isset($args['throwable'])) { + $this->assertEquals($args['throwable'], $exception->getPrevious()); + } + } + + /** + * Data provider for testing http_exception instances. + * + * @return array the test case data. + */ + public function exception_data_provider() { + return [ + 'With only the response object' => [ + 'args' => [ + 'response' => new http_response( + ['body' => '', 'headers' => ['Content-Type' => 'application/json']], + 401 + ) + ] + ], + 'With response and message, code and throwable omitted' => [ + 'args' => [ + 'response' => new http_response( + ['body' => '', 'headers' => ['Content-Type' => 'application/json']], + 401 + ), + 'message' => 'HTTP error: 401 Unauthorised' + ] + ], + 'With response, message and code, throwable omitted' => [ + 'args' => [ + 'response' => new http_response( + ['body' => '', 'headers' => ['Content-Type' => 'application/json']], + 401 + ), + 'message' => 'HTTP error: 401 Unauthorised', + 'code' => 401 + ] + ], + 'With response, message, code, throwable' => [ + 'args' => [ + 'response' => new http_response( + ['body' => '', 'headers' => ['Content-Type' => 'application/json']], + 401 + ), + 'message' => 'HTTP error: 401 Unauthorised', + 'code' => 401, + 'throwable' => new \Exception('another exception') + ] + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/lib/http_response_test.php b/enrol/lti/tests/local/ltiadvantage/lib/http_response_test.php new file mode 100644 index 00000000000..05ec320b92e --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/lib/http_response_test.php @@ -0,0 +1,81 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +/** + * Tests for the http_response class. + * + * @package enrol_lti + * @copyright 2022 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\lib\http_response + */ +class http_response_test extends \basic_testcase { + + /** + * Test constructor and getters for a range of inputs. + * + * @dataProvider response_data_provider + * @param array $payload the array of header and body payload data. + * @param int $status the int status of the http response. + * @covers ::__construct + */ + public function test_response(array $payload, int $status) { + $response = new http_response($payload, $status); + $this->assertEquals($payload['headers'], $response->getHeaders()); + $this->assertEquals($payload['body'], $response->getBody()); + $this->assertEquals($status, $response->getStatusCode()); + } + + /** + * Data provider for testing http_response instances. + * + * @return array the test case data. + */ + public function response_data_provider() { + return [ + 'valid headers and body' => [ + 'payload' => [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => '{"something": true}' + ], + 'httpstatus' => 200 + ], + 'valid headers with empty body' => [ + 'payload' => [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => '' + ], + 'httpstatus' => 200 + ], + 'valid, no headers or body' => [ + 'payload' => [ + 'headers' => [], + 'body' => '' + ], + 'httpstatus' => 200 + ], + 'valid headers, empty body, non-200 response status' => [ + 'payload' => [ + 'headers' => ['httpstatus' => 'HTTP/1.1 401 Unauthorised: message ', 'Content-Type' => 'application/json'], + 'body' => '' + ], + 'httpstatus' => 401 + ] + ]; + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/lib/issuer_database_test.php b/enrol/lti/tests/local/ltiadvantage/lib/issuer_database_test.php new file mode 100644 index 00000000000..fea67d6075e --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/lib/issuer_database_test.php @@ -0,0 +1,102 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +use enrol_lti\local\ltiadvantage\entity\application_registration; +use enrol_lti\local\ltiadvantage\repository\application_registration_repository; +use enrol_lti\local\ltiadvantage\repository\deployment_repository; +use Packback\Lti1p3\LtiDeployment; +use Packback\Lti1p3\LtiRegistration; + +/** + * Tests for the issuer_database class. + * + * @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\lib\issuer_database + */ +class issuer_database_test extends \advanced_testcase { + + /** + * Test the Moodle implementation of the library database method test_find_registration_by_issuer(). + * + * @covers ::findRegistrationByIssuer + */ + public function test_find_registration_by_issuer() { + $this->resetAfterTest(); + $appregrepo = new application_registration_repository(); + $appreg = application_registration::create( + 'My platform', + new \moodle_url('https://lms.example.com'), + 'client-id-123', + new \moodle_url('https://lms.example.com/lti/auth'), + new \moodle_url('https://lms.example.com/lti/jwks'), + new \moodle_url('https://lms.example.com/lti/token') + ); + $appregrepo->save($appreg); + + $issuerdb = new issuer_database($appregrepo, new deployment_repository()); + $registration = $issuerdb->findRegistrationByIssuer('https://lms.example.com', 'client-id-123'); + $this->assertInstanceOf(LtiRegistration::class, $registration); + $this->assertEquals($appreg->get_authenticationrequesturl()->out(false), $registration->getAuthLoginUrl()); + $this->assertEquals($appreg->get_jwksurl()->out(false), $registration->getKeySetUrl()); + $this->assertEquals($appreg->get_accesstokenurl()->out(false), $registration->getAuthTokenUrl()); + $this->assertEquals($appreg->get_clientid(), $registration->getClientId()); + $this->assertEquals($appreg->get_platformid()->out(false), $registration->getIssuer()); + + $this->assertNull($issuerdb->findRegistrationByIssuer('https://lms.example.com', 'client-id-456')); + + $this->expectException(\coding_exception::class); + $this->expectExceptionMessageMatches('/Both issuer and client id are required to identify platform /'); + $issuerdb->findRegistrationByIssuer('https://lms.example.com'); + } + + /** + * Test the Moodle implementation of the library database method test_find_deployment(). + * + * @covers ::findDeployment + */ + public function test_find_deployment() { + $this->resetAfterTest(); + $appregrepo = new application_registration_repository(); + $appreg = application_registration::create( + 'My platform', + new \moodle_url('https://lms.example.com'), + 'client-id-123', + new \moodle_url('https://lms.example.com/lti/auth'), + new \moodle_url('https://lms.example.com/lti/jwks'), + new \moodle_url('https://lms.example.com/lti/token') + ); + $appreg = $appregrepo->save($appreg); + $dep = $appreg->add_tool_deployment('Site wide tool deployment', 'deployment-id-1'); + $deploymentrepo = new deployment_repository(); + $deploymentrepo->save($dep); + + $issuerdb = new issuer_database($appregrepo, new deployment_repository()); + $deployment = $issuerdb->findDeployment('https://lms.example.com', 'deployment-id-1', 'client-id-123'); + $this->assertInstanceOf(LtiDeployment::class, $deployment); + $this->assertEquals($dep->get_deploymentid(), $deployment->getDeploymentId()); + + $this->assertNull($issuerdb->findDeployment('https://lms.example.com', 'deployment-id-1', 'client-id-456')); + $this->assertNull($issuerdb->findDeployment('https://lms.example.com', 'deployment-id-2', 'client-id-123')); + + $this->expectException(\coding_exception::class); + $this->expectExceptionMessageMatches('/Both issuer and client id are required to identify platform /'); + $issuerdb->findDeployment('https://lms.example.com', 'deployment-id-2'); + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/lib/launch_cache_session_test.php b/enrol/lti/tests/local/ltiadvantage/lib/launch_cache_session_test.php new file mode 100644 index 00000000000..a1bf00ad92b --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/lib/launch_cache_session_test.php @@ -0,0 +1,78 @@ +. + +namespace enrol_lti\local\ltiadvantage\lib; + +/** + * Tests for the launch_cache_session class. + * + * @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\lib\launch_cache_session + */ +class launch_cache_session_test extends \advanced_testcase { + + /** + * Test that the session cache, and in particular distinct object instances, can cache and retrieve launch data. + * + * Using different objects simulates the kind of usage we expect: uses across different requests. + * + * @covers ::cacheLaunchData + */ + public function test_cache_launch_data() { + $lcs = new launch_cache_session(); + $lcs->cacheLaunchData('TestKey', ['JWT body' => 'xxx']); + + $lcs2 = new launch_cache_session(); + $this->assertEquals(['JWT body' => 'xxx'], $lcs2->getLaunchData('TestKey')); + $this->assertNull($lcs2->getLaunchData('TestKey123')); + } + + /** + * Test that the session cache, and in particular distinct object instances, can cache and check the nonce data. + * + * Using different objects simulates the kind of usage we expect: uses across different requests. + * + * @covers ::cacheNonce + */ + public function test_cache_and_check_nonce() { + $lcs = new launch_cache_session(); + $lcs->cacheNonce('my_nonce_123', 'my_state_234'); + + $lcs2 = new launch_cache_session(); + $this->assertTrue($lcs2->checkNonceIsValid('my_nonce_123', 'my_state_234')); + $this->assertFalse($lcs2->checkNonceIsValid('different_nonce', 'my_state_234')); + $this->assertFalse($lcs2->checkNonceIsValid('my_nonce_123', 'different_state')); + } + + /** + * Test that the session cache, and in particular distinct object instances, can purge cached launch data. + * + * Using different objects simulates the kind of usage we expect: uses across different requests. + * + * @covers ::purge + */ + public function test_purge() { + $lcs = new launch_cache_session(); + $lcs->cacheLaunchData('TestKey', ['JWT body' => 'xxx']); + + $lcs2 = new launch_cache_session(); + $this->assertEquals(['JWT body' => 'xxx'], $lcs2->getLaunchData('TestKey')); + $lcs2->purge(); + $this->assertNull($lcs2->getLaunchData('TestKey')); + } +}