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 communication_matrix;
  18  
  19  use core\context;
  20  use core_communication\api;
  21  use core_communication\communication_test_helper_trait;
  22  use core_communication\processor;
  23  use stored_file;
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once (__DIR__ . '/matrix_test_helper_trait.php');
  28  require_once (__DIR__ . '/../../../tests/communication_test_helper_trait.php');
  29  
  30  /**
  31   * Class communication_feature_test to test the matrix features implemented using the core interfaces.
  32   *
  33   * @package    communication_matrix
  34   * @category   test
  35   * @copyright  2023 Safat Shahin <safat.shahin@moodle.com>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   * @covers \communication_matrix\communication_feature
  38   * @coversDefaultClass \communication_matrix\communication_feature
  39   */
  40  class communication_feature_test extends \advanced_testcase {
  41      use matrix_test_helper_trait;
  42      use communication_test_helper_trait;
  43  
  44      public function setUp(): void {
  45          parent::setUp();
  46          $this->resetAfterTest();
  47          $this->setup_communication_configs();
  48          $this->initialise_mock_server();
  49      }
  50  
  51      /**
  52       * Test create or update chat room.
  53       *
  54       * @covers ::create_chat_room
  55       */
  56      public function test_create_chat_room(): void {
  57          // Set up the test data first.
  58          $communication = \core_communication\api::load_by_instance(
  59              context: \core\context\system::instance(),
  60              component: 'communication_matrix',
  61              instancetype: 'example',
  62              instanceid: 1,
  63              provider: 'communication_matrix',
  64          );
  65  
  66          $communication->create_and_configure_room(
  67              communicationroomname: 'Room name',
  68              instance: (object) [
  69                  'matrixroomtopic' => 'A fun topic',
  70              ],
  71          );
  72  
  73          // phpcs:ignore moodle.Commenting.InlineComment.DocBlock
  74          /** @var communication_feature */
  75          $provider = $communication->get_room_provider();
  76          $this->assertInstanceOf(
  77              communication_feature::class,
  78              $provider,
  79          );
  80  
  81          // Run the create_chat_room task.
  82          $result = $provider->create_chat_room();
  83          $this->assertTrue($result);
  84  
  85          // Ensure that a room_id was set.
  86          $this->assertNotEmpty($provider->get_room_id());
  87  
  88          // Fetch the back office room data.
  89          $remoteroom = $this->backoffice_get_room();
  90  
  91          // The roomid set in the database must match the one set on the remote server.
  92          $this->assertEquals(
  93              $remoteroom->room_id,
  94              $provider->get_room_id(),
  95          );
  96  
  97          // The name is a feature of the communication API itself.
  98          $this->assertEquals(
  99              'Room name',
 100              $communication->get_room_name(),
 101          );
 102          $this->assertEquals(
 103              $communication->get_room_name(),
 104              $remoteroom->name,
 105          );
 106  
 107          // The topic is a Matrix feature.
 108          $roomconfig = $provider->get_room_configuration();
 109          $this->assertEquals(
 110              'A fun topic',
 111              $roomconfig->get_topic(),
 112          );
 113          $this->assertEquals(
 114              $remoteroom->topic,
 115              $roomconfig->get_topic(),
 116          );
 117  
 118          // The avatar features are checked in a separate test.
 119      }
 120  
 121      /**
 122       * Test update of a chat room.
 123       *
 124       * @covers ::update_chat_room
 125       */
 126      public function test_update_chat_room(): void {
 127          $communication = $this->create_room(
 128              roomname: 'Our room name',
 129              roomtopic: 'Our room topic',
 130          );
 131  
 132          // phpcs:ignore moodle.Commenting.InlineComment.DocBlock
 133          /** @var communication_feature */
 134          $provider = $communication->get_room_provider();
 135          $this->assertInstanceOf(
 136              communication_feature::class,
 137              $provider,
 138          );
 139  
 140          // Update the room name.
 141          // Note: We have to update the record via the API, and then call the provider update method.
 142          // That's because the update is performed asynchronously.
 143          $communication->update_room(
 144              communicationroomname: 'Our updated room name',
 145          );
 146          $provider->reload();
 147  
 148          // Now call the provider's update method.
 149          $provider->update_chat_room();
 150  
 151          // And assert that it was updated remotely.
 152          $remoteroom = $this->backoffice_get_room();
 153  
 154          $this->assertEquals(
 155              'Our updated room name',
 156              $communication->get_room_name(),
 157          );
 158          $this->assertEquals(
 159              $communication->get_room_name(),
 160              $remoteroom->name,
 161          );
 162          // The remote topic should not have changed.
 163          $this->assertEquals(
 164              'Our room topic',
 165              $remoteroom->topic,
 166          );
 167  
 168          // Now update just the topic.
 169          // First in the local API.
 170          $communication->update_room(
 171              instance: (object) [
 172                  'matrixroomtopic' => 'Our updated room topic',
 173              ],
 174          );
 175  
 176          $provider->reload();
 177  
 178          // Then call the provider's update method to actually perform the change.
 179          $provider->update_chat_room();
 180  
 181          // And assert that it was updated remotely.
 182          $remoteroom = $this->backoffice_get_room();
 183  
 184          $this->assertEquals(
 185              'Our updated room topic',
 186              $provider->get_room_configuration()->get_topic(),
 187          );
 188  
 189          // The remote topic should have been updated.
 190          $this->assertEquals(
 191              'Our updated room topic',
 192              $remoteroom->topic,
 193          );
 194  
 195          // The name should not have changed.
 196          $this->assertEquals(
 197              'Our updated room name',
 198              $communication->get_room_name(),
 199          );
 200      }
 201  
 202      /**
 203       * Test delete chat room.
 204       *
 205       * @covers ::delete_chat_room
 206       */
 207      public function test_delete_chat_room(): void {
 208          $communication = $this->create_room();
 209  
 210          $processor = $communication->get_processor();
 211          $provider = $communication->get_room_provider();
 212          $room = matrix_room::load_by_processor_id($processor->get_id());
 213  
 214          // Run the delete method.
 215          $this->assertTrue($provider->delete_chat_room());
 216  
 217          // The record of the room should have been removed.
 218          $this->assertNull(matrix_room::load_by_processor_id($processor->get_id()));
 219  
 220          // But the room itself shoudl exist.
 221          $matrixroomdata = $this->get_matrix_room_data($room->get_room_id());
 222  
 223          $this->assertNotEmpty($matrixroomdata);
 224          $this->assertEquals($processor->get_room_name(), $matrixroomdata->name);
 225          $this->assertEquals($room->get_topic(), $matrixroomdata->topic);
 226      }
 227  
 228      /**
 229       * Test update room avatar.
 230       *
 231       * @covers ::update_room_avatar
 232       * @dataProvider avatar_provider
 233       */
 234      public function test_update_room_avatar(
 235          ?string $before,
 236          ?string $after,
 237      ): void {
 238          $this->setAdminUser();
 239  
 240          // Create a new draft file.
 241          $logo = $this->create_communication_file('moodle_logo.jpg', 'logo.jpg');
 242          $circle = $this->create_communication_file('circle.png', 'circle.png');
 243  
 244          if ($before === 'logo') {
 245              $before = $logo;
 246          } else if ($before === 'circle') {
 247              $before = $circle;
 248          }
 249  
 250          if ($after === 'logo') {
 251              $after = $logo;
 252          } else if ($after === 'circle') {
 253              $after = $circle;
 254          }
 255  
 256          $communication = $this->create_matrix_room(
 257              component: 'communication_matrix',
 258              itemtype: 'example_room',
 259              itemid: 1,
 260              roomname: 'Example room name',
 261              roomavatar: $before,
 262          );
 263  
 264          // Confirm that the avatar was set remotely.
 265          $remoteroom = $this->backoffice_get_room();
 266  
 267          if ($before) {
 268              $this->assertStringEndsWith($before->get_filename(), $remoteroom->avatar);
 269              $avatarcontent = download_file_content($remoteroom->avatar);
 270              $this->assertEquals($before->get_content(), $avatarcontent);
 271          } else {
 272              $this->assertEmpty($remoteroom->avatar);
 273          }
 274  
 275          // Reload the API instance as the information stored has changed.
 276          $communication->reload();
 277  
 278          // Update the avatar with the 'after' avatar.
 279          $communication->update_room(
 280              avatar: $after,
 281          );
 282          $this->run_all_adhoc_tasks();
 283  
 284          // Confirm that the avatar was updated remotely.
 285          $remoteroom = $this->backoffice_get_room();
 286  
 287          if ($after) {
 288              $this->assertStringEndsWith($after->get_filename(), $remoteroom->avatar);
 289              $avatarcontent = download_file_content($remoteroom->avatar);
 290              $this->assertEquals($after->get_content(), $avatarcontent);
 291          } else {
 292              $this->assertEmpty($remoteroom->avatar);
 293          }
 294      }
 295  
 296      /**
 297       * Tests for setting and updating the room avatar.
 298       *
 299       * @return array
 300       */
 301      public static function avatar_provider(): array {
 302          return [
 303              'Empty to avatar' => [
 304                  null,
 305                  'circle',
 306              ],
 307              'Avatar to empty' => [
 308                  'circle',
 309                  null,
 310              ],
 311              'Avatar to new avatar' => [
 312                  'circle',
 313                  'logo',
 314              ],
 315          ];
 316      }
 317  
 318      /**
 319       * Test get chat room url.
 320       *
 321       * @covers ::get_chat_room_url
 322       */
 323      public function test_get_chat_room_url(): void {
 324          $communication = $this->create_room();
 325  
 326          $provider = $communication->get_room_provider();
 327  
 328          $url = $provider->get_chat_room_url();
 329          $this->assertNotNull($url);
 330  
 331          // Fetch the room information from the server.
 332          $remoteroom = $this->backoffice_get_room();
 333  
 334          $this->assertStringEndsWith(
 335              $remoteroom->room_id,
 336              $url,
 337          );
 338      }
 339  
 340      /**
 341       * Test create members.
 342       *
 343       * @covers ::create_members
 344       * @covers ::add_registered_matrix_user_to_room
 345       */
 346      public function test_create_members(): void {
 347          $user = $this->getDataGenerator()->create_user();
 348  
 349          $communication = $this->create_room(
 350              members: [
 351                  $user->id,
 352              ],
 353          );
 354  
 355          $remoteroom = $this->backoffice_get_room();
 356          $this->assertCount(1, $remoteroom->members);
 357          $member = reset($remoteroom->members);
 358          $this->assertStringStartsWith("@{$user->username}", $member->userid);
 359      }
 360  
 361      /**
 362       * Test add/remove members from room.
 363       *
 364       * @covers ::remove_members_from_room
 365       * @covers ::add_members_to_room
 366       * @covers ::add_registered_matrix_user_to_room
 367       * @covers ::check_room_membership
 368       * @covers ::set_matrix_power_levels
 369       */
 370      public function test_add_and_remove_members_from_room(): void {
 371          $user = $this->getDataGenerator()->create_user();
 372          $user2 = $this->getDataGenerator()->create_user();
 373  
 374          $communication = $this->create_room();
 375          $provider = $communication->get_room_user_provider();
 376  
 377          $remoteroom = $this->backoffice_get_room();
 378          $this->assertCount(0, $remoteroom->members);
 379  
 380          // Add the members to the room.
 381          $provider->add_members_to_room([$user->id, $user2->id]);
 382  
 383          // Ensure that they have been created.
 384          $remoteroom = $this->backoffice_get_room();
 385          $this->assertCount(2, $remoteroom->members);
 386  
 387          $userids = array_map(fn($member) => $member->userid, $remoteroom->members);
 388          $userids = array_map(fn($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
 389          $this->assertContains("@{$user->username}", $userids);
 390          $this->assertContains("@{$user2->username}", $userids);
 391  
 392          // Remove member from matrix room.
 393          $provider->remove_members_from_room([$user->id]);
 394  
 395          // Ensure that they have been removed.
 396          $remoteroom = $this->backoffice_get_room();
 397          $members = (array) $remoteroom->members;
 398          $this->assertCount(1, $members);
 399          $userids = array_map(fn ($member) => $member->userid, $members);
 400          $userids = array_map(fn ($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
 401          $this->assertNotContains("@{$user->username}", $userids);
 402          $this->assertContains("@{$user2->username}", $userids);
 403      }
 404  
 405      /**
 406       * Test update of room membership.
 407       *
 408       * @covers ::update_room_membership
 409       * @covers ::set_matrix_power_levels
 410       * @covers ::is_power_levels_update_required
 411       * @covers ::get_user_allowed_power_level
 412       */
 413      public function test_update_room_membership(): void {
 414          $this->resetAfterTest();
 415  
 416          global $DB;
 417  
 418          // Create a new room.
 419          $course = $this->get_course('Sampleroom', 'none');
 420          $coursecontext = \context_course::instance($course->id);
 421          $user = $this->get_user();
 422  
 423          $communication = $this->create_room(
 424              component: 'core_course',
 425              itemtype: 'coursecommunication',
 426              itemid: $course->id,
 427              roomname: 'sampleroom',
 428              roomtopic: 'sampltopic',
 429              roomavatar: null,
 430              members: [$user->id],
 431              context: $coursecontext,
 432          );
 433  
 434          $provider = $communication->get_room_user_provider();
 435  
 436          // Add the members to the room.
 437          $provider->add_members_to_room([$user->id]);
 438  
 439          // Assign teacher role to the user.
 440          $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
 441          $this->getDataGenerator()->enrol_user($user->id, $course->id);
 442          role_assign($teacherrole->id, $user->id, $coursecontext->id);
 443  
 444          // Test the tasks added as the role is a teacher.
 445          $provider->update_room_membership([$user->id]);
 446  
 447          $processor = \core_communication\processor::load_by_instance(
 448              context: $coursecontext,
 449              component: 'core_course',
 450              instancetype: 'coursecommunication',
 451              instanceid: $course->id,
 452          );
 453          $synceduser = $processor->get_instance_userids(
 454              synced: true,
 455          );
 456          $synceduser = reset($synceduser);
 457  
 458          // Test if the communication user record is synced.
 459          $this->assertEquals($user->id, $synceduser);
 460      }
 461  
 462      /**
 463       * Test the user power level allocation according to context.
 464       *
 465       * @covers ::get_user_allowed_power_level
 466       */
 467      public function test_get_user_allowed_power_level(): void {
 468          $this->resetAfterTest();
 469          global $DB;
 470  
 471          // Create users.
 472          $user1 = $this->getDataGenerator()->create_user();
 473          $user2 = $this->getDataGenerator()->create_user();
 474  
 475          $course = $this->get_course();
 476          $coursecontext = \context_course::instance($course->id);
 477          $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
 478          $studentrole = $DB->get_record('role', ['shortname' => 'student']);
 479          $this->getDataGenerator()->enrol_user($user1->id, $course->id);
 480          $this->getDataGenerator()->enrol_user($user2->id, $course->id);
 481          // Assign roles.
 482          role_assign($teacherrole->id, $user1->id, $coursecontext->id);
 483          role_assign($studentrole->id, $user2->id, $coursecontext->id);
 484  
 485          $communicationprocessor = processor::load_by_instance(
 486              context: \core\context\course::instance($course->id),
 487              component: 'core_course',
 488              instancetype: 'coursecommunication',
 489              instanceid: $course->id
 490          );
 491  
 492          // Test if the power level is set according to the context.
 493          $this->assertEquals(
 494              matrix_constants::POWER_LEVEL_MOODLE_MODERATOR,
 495              $communicationprocessor->get_room_provider()->get_user_allowed_power_level($user1->id)
 496          );
 497          $this->assertEquals(
 498              matrix_constants::POWER_LEVEL_DEFAULT,
 499              $communicationprocessor->get_room_provider()->get_user_allowed_power_level($user2->id)
 500          );
 501      }
 502  
 503      /**
 504       * Helper to create a room.
 505       *
 506       * @param null|string $component
 507       * @param null|string $itemtype
 508       * @param null|int $itemid
 509       * @param null|string $roomname
 510       * @param null|string $roomtopic
 511       * @param null|stored_file $roomavatar
 512       * @param array $members
 513       * @return api
 514       */
 515      protected function create_room(
 516          ?string $component = 'communication_matrix',
 517          ?string $itemtype = 'example',
 518          ?int $itemid = 1,
 519          ?string $roomname = null,
 520          ?string $roomtopic = null,
 521          ?\stored_file $roomavatar = null,
 522          array $members = [],
 523          ?context $context = null,
 524      ): \core_communication\api {
 525          // Create a new room.
 526          $communication = \core_communication\api::load_by_instance(
 527              context: $context ?? \core\context\system::instance(),
 528              component: $component,
 529              instancetype: $itemtype,
 530              instanceid: $itemid,
 531              provider: 'communication_matrix',
 532          );
 533  
 534          $communication->create_and_configure_room(
 535              communicationroomname: $roomname ?? 'Room name',
 536              avatar: $roomavatar,
 537              instance: (object) [
 538                  'matrixroomtopic' => $roomtopic ?? 'A fun topic',
 539              ],
 540          );
 541  
 542          $communication->add_members_to_room($members);
 543  
 544          // Run the adhoc task.
 545          $this->run_all_adhoc_tasks();
 546  
 547          $communication->reload();
 548          return $communication;
 549      }
 550  
 551      /**
 552       * Test if the selected provider is configured.
 553       *
 554       * @covers ::is_configured
 555       */
 556      public function test_is_configured(): void {
 557          $course = $this->get_course();
 558          $communicationprocessor = processor::load_by_instance(
 559              context: \core\context\course::instance($course->id),
 560              component: 'core_course',
 561              instancetype: 'coursecommunication',
 562              instanceid: $course->id
 563          );
 564          $this->assertTrue($communicationprocessor->get_room_provider()->is_configured());
 565  
 566          // Unset communication_matrix settings.
 567          unset_config('matrixhomeserverurl', 'communication_matrix');
 568          unset_config('matrixaccesstoken', 'communication_matrix');
 569          $this->assertFalse($communicationprocessor->get_room_provider()->is_configured());
 570      }
 571  }