Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core\moodlenet;
  18  
  19  use context_course;
  20  use core\http_client;
  21  use core\oauth2\issuer;
  22  use GuzzleHttp\Exception\ClientException;
  23  use GuzzleHttp\Handler\MockHandler;
  24  use GuzzleHttp\HandlerStack;
  25  use GuzzleHttp\Psr7\Response;
  26  use PHPUnit\Framework\MockObject\MockObject;
  27  use Psr\Http\Message\ResponseInterface;
  28  use ReflectionMethod;
  29  use stdClass;
  30  use testing_data_generator;
  31  
  32  /**
  33   * Test coverage for moodlenet course sender.
  34   *
  35   * @package   core
  36   * @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @coversDefaultClass \core\moodlenet\course_sender
  39   */
  40  class course_sender_test extends \advanced_testcase {
  41  
  42      /** @var testing_data_generator Data generator. */
  43      private testing_data_generator $generator;
  44      /** @var stdClass Course object. */
  45      private stdClass $course;
  46  
  47      /** @var context_course Course context instance. */
  48      private context_course $coursecontext;
  49      /** @var issuer $issuer Dummy issuer. */
  50      private issuer $issuer;
  51      /** @var MockObject $mockoauthclient Mock OAuth client. */
  52      private MockObject $mockoauthclient;
  53  
  54      public static function setUpBeforeClass(): void {
  55          parent::setUpBeforeClass();
  56  
  57          require_once (__DIR__ . '/helpers.php');
  58      }
  59  
  60      /**
  61       * Set up function for tests.
  62       */
  63      protected function setUp(): void {
  64          parent::setUp();
  65  
  66          $this->resetAfterTest();
  67          // Get data generator.
  68          $this->generator = $this->getDataGenerator();
  69          // Create course.
  70          $this->course = $this->generator->create_course(['shortname' => 'testcourse']);
  71          $this->coursecontext = context_course::instance($this->course->id);
  72          // Create mock issuer.
  73          $this->issuer = helpers::get_mock_issuer(1);
  74          // Create mock builder for OAuth2 client.
  75          $mockbuilder = $this->getMockBuilder('core\oauth2\client');
  76          $mockbuilder->onlyMethods(['get_issuer', 'is_logged_in', 'get_accesstoken']);
  77          $mockbuilder->setConstructorArgs([$this->issuer, '', '']);
  78          // Get the OAuth2 client mock.
  79          $this->mockoauthclient = $mockbuilder->getMock();
  80      }
  81  
  82      /**
  83       * Test prepare_share_contents method.
  84       *
  85       * @covers ::prepare_share_contents
  86       */
  87      public function test_prepare_share_contents(): void {
  88          global $USER;
  89          $this->setAdminUser();
  90  
  91          // Set get_file method accessibility.
  92          $method = new ReflectionMethod(course_sender::class, 'prepare_share_contents');
  93          $method->setAccessible(true);
  94  
  95          // Test with invalid share format.
  96          $this->expectException(\moodle_exception::class);
  97          $this->expectExceptionMessage(get_string('moodlenet:invalidshareformat', 'error'));
  98  
  99          $httpclient = new http_client();
 100          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 101          $coursesender = new course_sender(
 102              random_int(5, 30),
 103              $USER->id,
 104              $moodlenetclient,
 105              $this->mockoauthclient,
 106              random_int(5, 30)
 107          );
 108          $coursesender = $method->invoke(new course_sender(
 109              $this->course->id,
 110              $USER->id,
 111              $moodlenetclient,
 112              $this->mockoauthclient,
 113              resource_sender::SHARE_FORMAT_BACKUP
 114          ));
 115  
 116          // Test with valid share format and invalid course module.
 117          $package = $method->invoke(new course_sender(
 118              $this->course->id,
 119              $USER->id,
 120              $moodlenetclient,
 121              $this->mockoauthclient,
 122              resource_sender::SHARE_FORMAT_BACKUP
 123          ));
 124          $this->assertEmpty($package);
 125  
 126          // Test with valid share format and valid course module.
 127          $package = $method->invoke(new course_sender(
 128              $this->course->id,
 129              $USER->id,
 130              $moodlenetclient,
 131              $this->mockoauthclient,
 132              resource_sender::SHARE_FORMAT_BACKUP
 133          ));
 134          $this->assertNotEmpty($package);
 135  
 136          // Confirm the expected stored_file object is returned.
 137          $this->assertInstanceOf(\stored_file::class, $package);
 138      }
 139  
 140      /**
 141       * Test get_resource_description method.
 142       *
 143       * @covers ::get_resource_description
 144       */
 145      public function test_get_resource_description(): void {
 146          global $USER;
 147          $this->setAdminUser();
 148  
 149          $course = $this->generator->create_course([
 150              'summary' => '<p>This is an example Moodle course description.</p>
 151  <p>&nbsp;</p>
 152  <p>This is a formatted intro</p>
 153  <p>&nbsp;</p>
 154  <p>This thing has many lines.</p>
 155  <p>&nbsp;</p>
 156  <p>The last word of this sentence is in <strong>bold</strong></p>'
 157          ]);
 158  
 159          // Set get_resource_description method accessibility.
 160          $method = new ReflectionMethod(course_sender::class, 'get_resource_description');
 161          $method->setAccessible(true);
 162  
 163          // Test the processed description.
 164          $httpclient = new http_client();
 165          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 166          $processeddescription = $method->invoke(new course_sender(
 167              $course->id,
 168              $USER->id,
 169              $moodlenetclient,
 170              $this->mockoauthclient,
 171              resource_sender::SHARE_FORMAT_BACKUP
 172          ), $this->coursecontext);
 173  
 174          $this->assertEquals('This is an example Moodle course description.
 175   
 176  This is a formatted intro
 177   
 178  This thing has many lines.
 179   
 180  The last word of this sentence is in bold', $processeddescription);
 181      }
 182  
 183      /**
 184       * Test share_resource() method.
 185       *
 186       * @dataProvider share_resource_provider
 187       * @covers ::share_resource
 188       * @covers ::log_event
 189       * @covers \core\moodlenet\moodlenet_client::create_resource_from_stored_file
 190       * @covers \core\moodlenet\moodlenet_client::prepare_file_share_request_data
 191       * @param ResponseInterface $httpresponse
 192       * @param array $expected
 193       */
 194      public function test_share_resource(ResponseInterface $httpresponse, array $expected): void {
 195          global $CFG, $USER;
 196          $this->setAdminUser();
 197  
 198          // Enable the experimental flag.
 199          $CFG->enablesharingtomoodlenet = true;
 200  
 201          // Set OAuth 2 service in the outbound setting to the dummy issuer.
 202          set_config('oauthservice', $this->issuer->get('id'), 'moodlenet');
 203  
 204          // Generate access token for the mock.
 205          $accesstoken = new stdClass();
 206          $accesstoken->token = random_string(64);
 207  
 208          // Get the OAuth2 client mock and set the return value for necessary methods.
 209          $this->mockoauthclient->method('get_issuer')->will($this->returnValue($this->issuer));
 210          $this->mockoauthclient->method('is_logged_in')->will($this->returnValue(true));
 211          $this->mockoauthclient->method('get_accesstoken')->will($this->returnValue($accesstoken));
 212  
 213          // Create Guzzle mock.
 214          $mockguzzlehandler = new MockHandler([$httpresponse]);
 215          $handlerstack = HandlerStack::create($mockguzzlehandler);
 216          $httpclient = new http_client(['handler' => $handlerstack]);
 217  
 218          // Create events sink.
 219          $sink = $this->redirectEvents();
 220  
 221          // Create sender.
 222          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 223          $coursesender = new course_sender(
 224              $this->course->id,
 225              $USER->id,
 226              $moodlenetclient,
 227              $this->mockoauthclient,
 228              resource_sender::SHARE_FORMAT_BACKUP
 229          );
 230  
 231          if (isset($expected['exception'])) {
 232              $this->expectException(ClientException::class);
 233              $this->expectExceptionMessage($expected['exception']);
 234          }
 235          // Call the API.
 236          $result = $coursesender->share_resource();
 237  
 238          // Verify the result.
 239          $this->assertEquals($expected['response_code'], $result['responsecode']);
 240          $this->assertEquals($expected['resource_url'], $result['drafturl']);
 241  
 242          // Verify the events.
 243          $events = $sink->get_events();
 244  
 245          $event = end($events);
 246          $this->assertInstanceOf('\core\event\moodlenet_resource_exported', $event);
 247          $this->assertEquals($USER->id, $event->userid);
 248  
 249          if ($result['responsecode'] == 201) {
 250              $description = "The user with id '{$USER->id}' successfully shared course to MoodleNet with the " .
 251                  "following course id, from context with id '{$this->coursecontext->id}': '{$this->course->id}'.";
 252          } else {
 253              $description = "The user with id '{$USER->id}' failed to share course to MoodleNet with the " .
 254                  "following course id, from context with id '{$this->coursecontext->id}': '{$this->course->id}'.";
 255          }
 256          $this->assertEquals($description, $event->get_description());
 257      }
 258  
 259      /**
 260       * Provider for test share_resource().
 261       *
 262       * @return array Test data.
 263       */
 264      public function share_resource_provider(): array {
 265          return [
 266              'Success' => [
 267                  'http_response' => new Response(
 268                      201,
 269                      ['Content-Type' => 'application/json'],
 270                      json_encode([
 271                          'homepage' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_1.mbz',
 272                      ]),
 273                  ),
 274                  'expected' => [
 275                      'response_code' => 201,
 276                      'resource_url' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_1.mbz',
 277                  ],
 278              ],
 279              'Fail with 200 status code' => [
 280                  'http_response' => new Response(
 281                      200,
 282                      ['Content-Type' => 'application/json'],
 283                      json_encode([
 284                          'homepage' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_2.mbz',
 285                      ]),
 286                  ),
 287                  'expected' => [
 288                      'response_code' => 200,
 289                      'resource_url' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_2.mbz',
 290                  ],
 291              ],
 292              'Fail with 401 status code' => [
 293                  'http_response' => new Response(
 294                      401,
 295                  ),
 296                  'expected' => [
 297                      'response_code' => 401,
 298                      'resource_url' => '',
 299                      'exception' => 'Client error: ' .
 300                          '`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
 301                          'resulted in a `401 Unauthorized` response',
 302                  ],
 303              ],
 304              'Fail with 404 status code' => [
 305                  'http_response' => new Response(
 306                      404,
 307                  ),
 308                  'expected' => [
 309                      'response_code' => 404,
 310                      'resource_url' => '',
 311                      'exception' => 'Client error: '.
 312                          '`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
 313                          'resulted in a `404 Not Found` response',
 314                  ],
 315              ],
 316          ];
 317      }
 318  
 319  
 320  }