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 moodle_exception;
  27  use PHPUnit\Framework\MockObject\MockObject;
  28  use Psr\Http\Message\ResponseInterface;
  29  use ReflectionMethod;
  30  use stdClass;
  31  use testing_data_generator;
  32  
  33  defined('MOODLE_INTERNAL') || die();
  34  
  35  global $CFG;
  36  
  37  require_once($CFG->dirroot . '/lib/tests/moodlenet/helpers.php');
  38  
  39  /**
  40   * Unit tests for {@see \core\moodlenet\course_partial_sender}.
  41   *
  42   * @coversDefaultClass \core\moodlenet\course_partial_sender
  43   * @package    core
  44   * @copyright  2023 Huong Nguyen <huongnv13@gmail.com>
  45   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class course_partial_sender_test extends \advanced_testcase {
  48  
  49      /** @var testing_data_generator Data generator. */
  50      private testing_data_generator $generator;
  51      /** @var stdClass Course object. */
  52      private stdClass $course;
  53      /** @var context_course Course context instance. */
  54      private context_course $coursecontext;
  55      /** @var array List of activities. */
  56      private array $activities;
  57      /** @var issuer $issuer Dummy issuer. */
  58      private issuer $issuer;
  59      /** @var MockObject $mockoauthclient Mock OAuth client. */
  60      private MockObject $mockoauthclient;
  61  
  62      /**
  63       * Set up function for tests.
  64       */
  65      protected function setUp(): void {
  66          parent::setUp();
  67  
  68          $this->resetAfterTest();
  69          // Get data generator.
  70          $this->generator = $this->getDataGenerator();
  71          // Create course.
  72          $this->course = $this->generator->create_course(['shortname' => 'testcourse']);
  73          $this->coursecontext = context_course::instance($this->course->id);
  74          // Create activities.
  75          $this->activities[1] = $this->generator->create_module('page', ['course' => $this->course->id]);
  76          $this->activities[2] = $this->generator->create_module('page', ['course' => $this->course->id]);
  77          // Create mock issuer.
  78          $this->issuer = helpers::get_mock_issuer(1);
  79          // Create mock builder for OAuth2 client.
  80          $mockbuilder = $this->getMockBuilder('core\oauth2\client');
  81          $mockbuilder->onlyMethods(['get_issuer', 'is_logged_in', 'get_accesstoken']);
  82          $mockbuilder->setConstructorArgs([$this->issuer, '', '']);
  83          // Get the OAuth2 client mock.
  84          $this->mockoauthclient = $mockbuilder->getMock();
  85      }
  86  
  87      /**
  88       * Test prepare_share_contents method.
  89       *
  90       * @covers ::prepare_share_contents
  91       */
  92      public function test_prepare_share_contents(): void {
  93          global $USER;
  94          $this->setAdminUser();
  95  
  96          // Set get_file method accessibility.
  97          $method = new ReflectionMethod(course_partial_sender::class, 'prepare_share_contents');
  98          $method->setAccessible(true);
  99  
 100          $httpclient = new http_client();
 101          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 102  
 103          $this->expectException(moodle_exception::class);
 104          $this->expectExceptionMessage(get_string('invalidcoursemodule', 'error'));
 105  
 106          // Test with valid share format and invalid course module id.
 107          $package = $method->invoke(new course_partial_sender(
 108              $this->course->id,
 109              $USER->id,
 110              $moodlenetclient,
 111              $this->mockoauthclient,
 112              [random_int(5, 30)],
 113              resource_sender::SHARE_FORMAT_BACKUP
 114          ));
 115          $this->assertEmpty($package);
 116  
 117          // Test with valid share format and valid course module ids.
 118          $package = $method->invoke(new course_partial_sender(
 119              $this->course->id,
 120              $USER->id,
 121              $moodlenetclient,
 122              $this->mockoauthclient,
 123              [$this->activities[1]->cmid, $this->activities[2]->cmid],
 124              resource_sender::SHARE_FORMAT_BACKUP
 125          ));
 126          $this->assertNotEmpty($package);
 127  
 128          // Confirm the expected stored_file object is returned.
 129          $this->assertInstanceOf(\stored_file::class, $package);
 130      }
 131  
 132      /**
 133       * Test get_resource_description method.
 134       *
 135       * @covers ::get_resource_description
 136       */
 137      public function test_get_resource_description(): void {
 138          global $USER;
 139          $this->setAdminUser();
 140  
 141          $course = $this->generator->create_course([
 142              'summary' => '<p>This is an example Moodle course description.</p>
 143  <p>&nbsp;</p>
 144  <p>This is a formatted intro</p>
 145  <p>&nbsp;</p>
 146  <p>This thing has many lines.</p>
 147  <p>&nbsp;</p>
 148  <p>The last word of this sentence is in <strong>bold</strong></p>'
 149          ]);
 150          $page = $this->generator->create_module('page', ['course' => $course->id]);
 151  
 152          // Set get_resource_description method accessibility.
 153          $method = new ReflectionMethod(course_partial_sender::class, 'get_resource_description');
 154          $method->setAccessible(true);
 155  
 156          // Test the processed description.
 157          $httpclient = new http_client();
 158          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 159          $processeddescription = $method->invoke(new course_partial_sender(
 160              $course->id,
 161              $USER->id,
 162              $moodlenetclient,
 163              $this->mockoauthclient,
 164              [$page->cmid],
 165              resource_sender::SHARE_FORMAT_BACKUP
 166          ), $this->coursecontext);
 167  
 168          $this->assertEquals('This is an example Moodle course description.
 169   
 170  This is a formatted intro
 171   
 172  This thing has many lines.
 173   
 174  The last word of this sentence is in bold', $processeddescription);
 175      }
 176  
 177      /**
 178       * Test share_resource() method.
 179       *
 180       * @dataProvider share_resource_provider
 181       * @covers ::share_resource
 182       * @covers ::log_event
 183       * @covers \core\moodlenet\moodlenet_client::create_resource_from_stored_file
 184       * @covers \core\moodlenet\moodlenet_client::prepare_file_share_request_data
 185       * @param ResponseInterface $httpresponse
 186       * @param array $expected
 187       */
 188      public function test_share_resource(ResponseInterface $httpresponse, array $expected): void {
 189          global $CFG, $USER;
 190          $this->setAdminUser();
 191  
 192          // Enable the experimental flag.
 193          $CFG->enablesharingtomoodlenet = true;
 194  
 195          // Set OAuth 2 service in the outbound setting to the dummy issuer.
 196          set_config('oauthservice', $this->issuer->get('id'), 'moodlenet');
 197  
 198          // Generate access token for the mock.
 199          $accesstoken = new stdClass();
 200          $accesstoken->token = random_string(64);
 201  
 202          // Get the OAuth2 client mock and set the return value for necessary methods.
 203          $this->mockoauthclient->method('get_issuer')->will($this->returnValue($this->issuer));
 204          $this->mockoauthclient->method('is_logged_in')->will($this->returnValue(true));
 205          $this->mockoauthclient->method('get_accesstoken')->will($this->returnValue($accesstoken));
 206  
 207          // Create Guzzle mock.
 208          $mockguzzlehandler = new MockHandler([$httpresponse]);
 209          $handlerstack = HandlerStack::create($mockguzzlehandler);
 210          $httpclient = new http_client(['handler' => $handlerstack]);
 211  
 212          // Create events sink.
 213          $sink = $this->redirectEvents();
 214  
 215          // Create sender.
 216          $moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
 217          $coursepartialsender = new course_partial_sender(
 218              $this->course->id,
 219              $USER->id,
 220              $moodlenetclient,
 221              $this->mockoauthclient,
 222              [$this->activities[1]->cmid, $this->activities[2]->cmid],
 223              resource_sender::SHARE_FORMAT_BACKUP
 224          );
 225  
 226          if (isset($expected['exception'])) {
 227              $this->expectException(ClientException::class);
 228              $this->expectExceptionMessage($expected['exception']);
 229          }
 230          // Call the API.
 231          $result = $coursepartialsender->share_resource();
 232  
 233          // Verify the result.
 234          $this->assertEquals($expected['response_code'], $result['responsecode']);
 235          $this->assertEquals($expected['resource_url'], $result['drafturl']);
 236  
 237          // Verify the events.
 238          $events = $sink->get_events();
 239  
 240          $event = end($events);
 241          $this->assertInstanceOf('\core\event\moodlenet_resource_exported', $event);
 242          $this->assertEquals($USER->id, $event->userid);
 243  
 244          $cmidslist = implode("', '", [$this->activities[1]->cmid, $this->activities[2]->cmid]);
 245          if ($result['responsecode'] == 201) {
 246              $description = "The user with id '{$USER->id}' successfully shared activities to MoodleNet with the " .
 247                  "following course module ids, from context with id '{$this->coursecontext->id}': '{$cmidslist}'.";
 248          } else {
 249              $description = "The user with id '{$USER->id}' failed to share activities to MoodleNet with the " .
 250                  "following course module ids, from context with id '{$this->coursecontext->id}': '{$cmidslist}'.";
 251          }
 252          $this->assertEquals($description, $event->get_description());
 253      }
 254  
 255      /**
 256       * Provider for test share_resource().
 257       *
 258       * @return array Test data.
 259       */
 260      public function share_resource_provider(): array {
 261          return [
 262              'Success' => [
 263                  'http_response' => new Response(
 264                      201,
 265                      ['Content-Type' => 'application/json'],
 266                      json_encode([
 267                          'homepage' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_1.mbz',
 268                      ]),
 269                  ),
 270                  'expected' => [
 271                      'response_code' => 201,
 272                      'resource_url' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_1.mbz',
 273                  ],
 274              ],
 275              'Fail with 200 status code' => [
 276                  'http_response' => new Response(
 277                      200,
 278                      ['Content-Type' => 'application/json'],
 279                      json_encode([
 280                          'homepage' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_2.mbz',
 281                      ]),
 282                  ),
 283                  'expected' => [
 284                      'response_code' => 200,
 285                      'resource_url' => 'https://moodlenet.example.com/drafts/view/testcourse_backup_2.mbz',
 286                  ],
 287              ],
 288              'Fail with 401 status code' => [
 289                  'http_response' => new Response(
 290                      401,
 291                  ),
 292                  'expected' => [
 293                      'response_code' => 401,
 294                      'resource_url' => '',
 295                      'exception' => 'Client error: ' .
 296                          '`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
 297                          'resulted in a `401 Unauthorized` response',
 298                  ],
 299              ],
 300              'Fail with 404 status code' => [
 301                  'http_response' => new Response(
 302                      404,
 303                  ),
 304                  'expected' => [
 305                      'response_code' => 404,
 306                      'resource_url' => '',
 307                      'exception' => 'Client error: '.
 308                          '`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
 309                          'resulted in a `404 Not Found` response',
 310                  ],
 311              ],
 312          ];
 313      }
 314  
 315  }