first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,66 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use communication_matrix\matrix_test_helper_trait;
use Moodle\BehatExtension\Exception\SkippedException;
require_once(__DIR__ . '/../matrix_test_helper_trait.php');
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../../tests/communication_test_helper_trait.php');
/**
* Class behat_communication_matrix for behat custom steps and configuration for matrix.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_communication_matrix extends \behat_base {
use \core_communication\communication_test_helper_trait;
use matrix_test_helper_trait;
/**
* BeforeScenario hook to reset the mock server.
*
* @BeforeScenario @communication_matrix
*
* @param BeforeScenarioScope $scope
*/
public function before_scenario(BeforeScenarioScope $scope) {
if (defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
$this->reset_mock();
}
}
/**
* Setup and configure and mock server for matrix.
*
* @Given /^a Matrix mock server is configured$/
*/
public function initialize_mock_server(): void {
if (!defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
throw new SkippedException(
'The TEST_COMMUNICATION_MATRIX_MOCK_SERVER constant must be defined to run communication_matrix tests'
);
}
$this->setup_communication_configs();
$this->initialise_mock_configs();
}
}
@@ -0,0 +1,34 @@
@communication @communication_matrix
Feature: Communication matrix form field
In order to create a new communication room in matrix
As a teacher
I can update the room the information from course
Background: Make sure the mock server is initialized and a course is created for teacher
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Test course | Test course | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | Test course | editingteacher |
@javascript
Scenario: I can add room name and topic for matrix room
Given a Matrix mock server is configured
And I am on the "Test course" "Course" page logged in as "teacher1"
When I navigate to "Communication" in current page administration
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
And I set the following fields to these values:
| communication_matrixroomname | Sampleroomname |
| matrixroomtopic | Sampleroomtopic |
And I should see "Room name"
And I should see "Room topic"
And I press "Save changes"
And I navigate to "Communication" in current page administration
Then the field "Room name" matches value "Sampleroomname"
And the field "Room topic" matches value "Sampleroomtopic"
@@ -0,0 +1,52 @@
@communication @communication_matrix
Feature: Display communication room status banner
Show a banner depending on the room status
As a teacher or admin
Background:
Given a Matrix mock server is configured
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category | selectedcommunication | communicationroomname |
| Test course | Test course | 0 | communication_matrix | matrixroom |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | Test course | editingteacher |
| student1 | Test course | student |
Scenario: I can see the room has been created and in a pending status
When I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
When I am on the "Test course" "Course" page logged in as "student1"
# Not for students to see.
Then I should not see "Your Matrix room will be ready soon." in the "page-content" "region"
Scenario: I can see the room has been created and ready to access
When I run all adhoc tasks
And I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room is ready." in the "page-content" "region"
# This is a one time message per user.
When I reload the page
Then I should not see "Your Matrix room is ready." in the "page-content" "region"
# Not for students to see.
When I am on the "Test course" "Course" page logged in as "student1"
Then I should not see "Your Matrix room is ready." in the "page-content" "region"
Scenario: Enabling or disabling the matrix plugin hides the banner accordingly
Given I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
When I log in as "admin"
And I navigate to "Plugins > Communication > Manage communication providers" in site administration
And I should see "Matrix"
And I click on "Disable" "link" in the "Matrix" "table_row"
And I am on the "Test course" "Course" page logged in as "teacher1"
And I should not see "Your Matrix room will be ready soon." in the "page-content" "region"
And I log in as "admin"
And I navigate to "Plugins > Communication > Manage communication providers" in site administration
And I should see "Matrix"
And I click on "Enable" "link" in the "Matrix" "table_row"
And I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
@@ -0,0 +1,571 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
use core\context;
use core_communication\api;
use core_communication\communication_test_helper_trait;
use core_communication\processor;
use stored_file;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/matrix_test_helper_trait.php');
require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php');
/**
* Class communication_feature_test to test the matrix features implemented using the core interfaces.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\communication_feature
* @coversDefaultClass \communication_matrix\communication_feature
*/
class communication_feature_test extends \advanced_testcase {
use matrix_test_helper_trait;
use communication_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test create or update chat room.
*
* @covers ::create_chat_room
*/
public function test_create_chat_room(): void {
// Set up the test data first.
$communication = \core_communication\api::load_by_instance(
context: \core\context\system::instance(),
component: 'communication_matrix',
instancetype: 'example',
instanceid: 1,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: 'Room name',
instance: (object) [
'matrixroomtopic' => 'A fun topic',
],
);
// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/** @var communication_feature */
$provider = $communication->get_room_provider();
$this->assertInstanceOf(
communication_feature::class,
$provider,
);
// Run the create_chat_room task.
$result = $provider->create_chat_room();
$this->assertTrue($result);
// Ensure that a room_id was set.
$this->assertNotEmpty($provider->get_room_id());
// Fetch the back office room data.
$remoteroom = $this->backoffice_get_room();
// The roomid set in the database must match the one set on the remote server.
$this->assertEquals(
$remoteroom->room_id,
$provider->get_room_id(),
);
// The name is a feature of the communication API itself.
$this->assertEquals(
'Room name',
$communication->get_room_name(),
);
$this->assertEquals(
$communication->get_room_name(),
$remoteroom->name,
);
// The topic is a Matrix feature.
$roomconfig = $provider->get_room_configuration();
$this->assertEquals(
'A fun topic',
$roomconfig->get_topic(),
);
$this->assertEquals(
$remoteroom->topic,
$roomconfig->get_topic(),
);
// The avatar features are checked in a separate test.
}
/**
* Test update of a chat room.
*
* @covers ::update_chat_room
*/
public function test_update_chat_room(): void {
$communication = $this->create_room(
roomname: 'Our room name',
roomtopic: 'Our room topic',
);
// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/** @var communication_feature */
$provider = $communication->get_room_provider();
$this->assertInstanceOf(
communication_feature::class,
$provider,
);
// Update the room name.
// Note: We have to update the record via the API, and then call the provider update method.
// That's because the update is performed asynchronously.
$communication->update_room(
communicationroomname: 'Our updated room name',
);
$provider->reload();
// Now call the provider's update method.
$provider->update_chat_room();
// And assert that it was updated remotely.
$remoteroom = $this->backoffice_get_room();
$this->assertEquals(
'Our updated room name',
$communication->get_room_name(),
);
$this->assertEquals(
$communication->get_room_name(),
$remoteroom->name,
);
// The remote topic should not have changed.
$this->assertEquals(
'Our room topic',
$remoteroom->topic,
);
// Now update just the topic.
// First in the local API.
$communication->update_room(
instance: (object) [
'matrixroomtopic' => 'Our updated room topic',
],
);
$provider->reload();
// Then call the provider's update method to actually perform the change.
$provider->update_chat_room();
// And assert that it was updated remotely.
$remoteroom = $this->backoffice_get_room();
$this->assertEquals(
'Our updated room topic',
$provider->get_room_configuration()->get_topic(),
);
// The remote topic should have been updated.
$this->assertEquals(
'Our updated room topic',
$remoteroom->topic,
);
// The name should not have changed.
$this->assertEquals(
'Our updated room name',
$communication->get_room_name(),
);
}
/**
* Test delete chat room.
*
* @covers ::delete_chat_room
*/
public function test_delete_chat_room(): void {
$communication = $this->create_room();
$processor = $communication->get_processor();
$provider = $communication->get_room_provider();
$room = matrix_room::load_by_processor_id($processor->get_id());
// Run the delete method.
$this->assertTrue($provider->delete_chat_room());
// The record of the room should have been removed.
$this->assertNull(matrix_room::load_by_processor_id($processor->get_id()));
// But the room itself shoudl exist.
$matrixroomdata = $this->get_matrix_room_data($room->get_room_id());
$this->assertNotEmpty($matrixroomdata);
$this->assertEquals($processor->get_room_name(), $matrixroomdata->name);
$this->assertEquals($room->get_topic(), $matrixroomdata->topic);
}
/**
* Test update room avatar.
*
* @covers ::update_room_avatar
* @dataProvider avatar_provider
*/
public function test_update_room_avatar(
?string $before,
?string $after,
): void {
$this->setAdminUser();
// Create a new draft file.
$logo = $this->create_communication_file('moodle_logo.jpg', 'logo.jpg');
$circle = $this->create_communication_file('circle.png', 'circle.png');
if ($before === 'logo') {
$before = $logo;
} else if ($before === 'circle') {
$before = $circle;
}
if ($after === 'logo') {
$after = $logo;
} else if ($after === 'circle') {
$after = $circle;
}
$communication = $this->create_matrix_room(
component: 'communication_matrix',
itemtype: 'example_room',
itemid: 1,
roomname: 'Example room name',
roomavatar: $before,
);
// Confirm that the avatar was set remotely.
$remoteroom = $this->backoffice_get_room();
if ($before) {
$this->assertStringEndsWith($before->get_filename(), $remoteroom->avatar);
$avatarcontent = download_file_content($remoteroom->avatar);
$this->assertEquals($before->get_content(), $avatarcontent);
} else {
$this->assertEmpty($remoteroom->avatar);
}
// Reload the API instance as the information stored has changed.
$communication->reload();
// Update the avatar with the 'after' avatar.
$communication->update_room(
avatar: $after,
);
$this->run_all_adhoc_tasks();
// Confirm that the avatar was updated remotely.
$remoteroom = $this->backoffice_get_room();
if ($after) {
$this->assertStringEndsWith($after->get_filename(), $remoteroom->avatar);
$avatarcontent = download_file_content($remoteroom->avatar);
$this->assertEquals($after->get_content(), $avatarcontent);
} else {
$this->assertEmpty($remoteroom->avatar);
}
}
/**
* Tests for setting and updating the room avatar.
*
* @return array
*/
public static function avatar_provider(): array {
return [
'Empty to avatar' => [
null,
'circle',
],
'Avatar to empty' => [
'circle',
null,
],
'Avatar to new avatar' => [
'circle',
'logo',
],
];
}
/**
* Test get chat room url.
*
* @covers ::get_chat_room_url
*/
public function test_get_chat_room_url(): void {
$communication = $this->create_room();
$provider = $communication->get_room_provider();
$url = $provider->get_chat_room_url();
$this->assertNotNull($url);
// Fetch the room information from the server.
$remoteroom = $this->backoffice_get_room();
$this->assertStringEndsWith(
$remoteroom->room_id,
$url,
);
}
/**
* Test create members.
*
* @covers ::create_members
* @covers ::add_registered_matrix_user_to_room
*/
public function test_create_members(): void {
$user = $this->getDataGenerator()->create_user();
$communication = $this->create_room(
members: [
$user->id,
],
);
$remoteroom = $this->backoffice_get_room();
$this->assertCount(1, $remoteroom->members);
$member = reset($remoteroom->members);
$this->assertStringStartsWith("@{$user->username}", $member->userid);
}
/**
* Test add/remove members from room.
*
* @covers ::remove_members_from_room
* @covers ::add_members_to_room
* @covers ::add_registered_matrix_user_to_room
* @covers ::check_room_membership
* @covers ::set_matrix_power_levels
*/
public function test_add_and_remove_members_from_room(): void {
$user = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$communication = $this->create_room();
$provider = $communication->get_room_user_provider();
$remoteroom = $this->backoffice_get_room();
$this->assertCount(0, $remoteroom->members);
// Add the members to the room.
$provider->add_members_to_room([$user->id, $user2->id]);
// Ensure that they have been created.
$remoteroom = $this->backoffice_get_room();
$this->assertCount(2, $remoteroom->members);
$userids = array_map(fn($member) => $member->userid, $remoteroom->members);
$userids = array_map(fn($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
$this->assertContains("@{$user->username}", $userids);
$this->assertContains("@{$user2->username}", $userids);
// Remove member from matrix room.
$provider->remove_members_from_room([$user->id]);
// Ensure that they have been removed.
$remoteroom = $this->backoffice_get_room();
$members = (array) $remoteroom->members;
$this->assertCount(1, $members);
$userids = array_map(fn ($member) => $member->userid, $members);
$userids = array_map(fn ($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
$this->assertNotContains("@{$user->username}", $userids);
$this->assertContains("@{$user2->username}", $userids);
}
/**
* Test update of room membership.
*
* @covers ::update_room_membership
* @covers ::set_matrix_power_levels
* @covers ::is_power_levels_update_required
* @covers ::get_user_allowed_power_level
*/
public function test_update_room_membership(): void {
$this->resetAfterTest();
global $DB;
// Create a new room.
$course = $this->get_course('Sampleroom', 'none');
$coursecontext = \context_course::instance($course->id);
$user = $this->get_user();
$communication = $this->create_room(
component: 'core_course',
itemtype: 'coursecommunication',
itemid: $course->id,
roomname: 'sampleroom',
roomtopic: 'sampltopic',
roomavatar: null,
members: [$user->id],
context: $coursecontext,
);
$provider = $communication->get_room_user_provider();
// Add the members to the room.
$provider->add_members_to_room([$user->id]);
// Assign teacher role to the user.
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
// Test the tasks added as the role is a teacher.
$provider->update_room_membership([$user->id]);
$processor = \core_communication\processor::load_by_instance(
context: $coursecontext,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$synceduser = $processor->get_instance_userids(
synced: true,
);
$synceduser = reset($synceduser);
// Test if the communication user record is synced.
$this->assertEquals($user->id, $synceduser);
}
/**
* Test the user power level allocation according to context.
*
* @covers ::get_user_allowed_power_level
*/
public function test_get_user_allowed_power_level(): void {
$this->resetAfterTest();
global $DB;
// Create users.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance($course->id);
$teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
$this->getDataGenerator()->enrol_user($user2->id, $course->id);
// Assign roles.
role_assign($teacherrole->id, $user1->id, $coursecontext->id);
role_assign($studentrole->id, $user2->id, $coursecontext->id);
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id
);
// Test if the power level is set according to the context.
$this->assertEquals(
matrix_constants::POWER_LEVEL_MOODLE_MODERATOR,
$communicationprocessor->get_room_provider()->get_user_allowed_power_level($user1->id)
);
$this->assertEquals(
matrix_constants::POWER_LEVEL_DEFAULT,
$communicationprocessor->get_room_provider()->get_user_allowed_power_level($user2->id)
);
}
/**
* Helper to create a room.
*
* @param null|string $component
* @param null|string $itemtype
* @param null|int $itemid
* @param null|string $roomname
* @param null|string $roomtopic
* @param null|stored_file $roomavatar
* @param array $members
* @return api
*/
protected function create_room(
?string $component = 'communication_matrix',
?string $itemtype = 'example',
?int $itemid = 1,
?string $roomname = null,
?string $roomtopic = null,
?\stored_file $roomavatar = null,
array $members = [],
?context $context = null,
): \core_communication\api {
// Create a new room.
$communication = \core_communication\api::load_by_instance(
context: $context ?? \core\context\system::instance(),
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: $roomname ?? 'Room name',
avatar: $roomavatar,
instance: (object) [
'matrixroomtopic' => $roomtopic ?? 'A fun topic',
],
);
$communication->add_members_to_room($members);
// Run the adhoc task.
$this->run_all_adhoc_tasks();
$communication->reload();
return $communication;
}
/**
* Test if the selected provider is configured.
*
* @covers ::is_configured
*/
public function test_is_configured(): void {
$course = $this->get_course();
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id
);
$this->assertTrue($communicationprocessor->get_room_provider()->is_configured());
// Unset communication_matrix settings.
unset_config('matrixhomeserverurl', 'communication_matrix');
unset_config('matrixaccesstoken', 'communication_matrix');
$this->assertFalse($communicationprocessor->get_room_provider()->is_configured());
}
}
@@ -0,0 +1,52 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix\tests\fixtures;
use core\http_client;
/**
* Tests for the api_base class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mocked_matrix_client extends \communication_matrix\matrix_client {
/**
* Public variant of the constructor.
*/
public function __construct() {
parent::__construct(...func_get_args());
}
/**
* Reset the test client.
*/
public static function reset_client(): void {
self::$client = null;
}
/**
* Set the http_client to the client specified.
*
* @param http_client $client
*/
public static function set_client(http_client $client): void {
self::$client = $client;
}
}
@@ -0,0 +1,411 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix\local;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use ReflectionMethod;
defined('MOODLE_INTERNAL') || die();
require_once(dirname(__DIR__) . '/matrix_client_test_trait.php');
/**
* Tests for the Matrix command class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\local\command
* @coversDefaultClass \communication_matrix\local\command
*/
class command_test extends \advanced_testcase {
use \communication_matrix\matrix_client_test_trait;
/**
* Test instantiation of a command when no method is provided.
*/
public function test_standard_instantiation(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
);
// Check the standard functionality.
$this->assertEquals('/example/endpoint', $command->getUri()->getPath());
$this->assertEquals('PUT', $command->getMethod());
$this->assertArrayHasKey('Authorization', $command->getHeaders());
}
/**
* Test instantiation of a command when no method is provided.
*/
public function test_instantiation_without_auth(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
requireauthorization: false,
);
// Check the standard functionality.
$this->assertEquals('/example/endpoint', $command->getUri()->getPath());
$this->assertEquals('PUT', $command->getMethod());
$this->assertArrayNotHasKey('Authorization', $command->getHeaders());
}
/**
* Test processing of command URL properties.
*
* @dataProvider url_parsing_provider
* @param string $url
* @param array $params
* @param string $expected
*/
public function test_url_parsing(
string $url,
array $params,
string $expected,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $url,
params: $params,
);
$this->assertEquals($expected, $command->getUri()->getPath());
}
/**
* Data provider for url parsing tests.
*
* @return array
*/
public static function url_parsing_provider(): array {
return [
[
'example/:id/endpoint',
[':id' => '39492'],
'/example/39492/endpoint',
],
[
'example/:id/endpoint/:id',
[':id' => '39492'],
'/example/39492/endpoint/39492',
],
[
'example/:id/endpoint/:id/:name',
[
':id' => '39492',
':name' => 'matrix',
],
'/example/39492/endpoint/39492/matrix',
],
];
}
/**
* Test processing of command URL properties with an array which contains untranslated parameters.
*/
public function test_url_parsing_extra_properties(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$this->expectException(\OutOfRangeException::class);
$this->expectExceptionMessage("URL contains untranslated parameters 'example/:id/endpoint'");
new command(
$instance,
method: 'PUT',
endpoint: 'example/:id/endpoint',
);
}
/**
* Test processing of command URL properties with an array which contains untranslated parameters.
*/
public function test_url_parsing_unused_properites(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$this->expectException(\OutOfRangeException::class);
$this->expectExceptionMessage("Parameter not found in URL ':id'");
new command(
$instance,
method: 'PUT',
endpoint: 'example/:ids/endpoint',
params: [
':id' => 12345,
],
);
}
/**
* Test the parameter fetching, processing, and parsing.
*
* @dataProvider parameter_and_option_provider
* @param string $endpoint
* @param array $params
* @param array $remainingparams
* @param array $allparams
* @param array $options
*/
public function test_parameters(
string $endpoint,
array $params,
array $remainingparams,
array $allparams,
array $options,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $endpoint,
params: $params,
);
$this->assertSame($remainingparams, $command->get_remaining_params());
$this->assertSame($allparams, $command->get_all_params());
$this->assertSame($options, $command->get_options());
}
/**
* Data provider for parameter tests.
*
* @return array
*/
public static function parameter_and_option_provider(): array {
$command = [
'method' => 'PUT',
'endpoint' => 'example/:id/endpoint',
];
return [
'no parameters' => [
'endpoint' => 'example/endpoint',
'params' => [],
'remainingparams' => [],
'allparams' => [],
'options' => [
'json' => [],
],
],
'named params' => [
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
],
'remainingparams' => [],
'allparams' => [
':id' => 12345,
],
'options' => [
'json' => [],
],
],
'mixture of params' => [
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'options' => [
'json' => [
'name' => 'matrix',
],
],
],
];
}
/**
* Test the query parameter handling.
*
* @dataProvider query_provider
* @param array $query
* @param string $expected
*/
public function test_query_parameters(
array $query,
string $expected,
): void {
// The query parameter is only added at the time we call send.
// That's because it can only be provided to Guzzle as an Option, not as part of the URL.
// Options can only be applied at time of transfer.
// Unfortuantely that leads to slightly less ideal testing that we'd like here.
$mock = new MockHandler();
$instance = $this->get_mocked_instance_for_version(
'v1.7',
mock: $mock,
);
$mock->append(function (Request $request) use ($expected): Response {
$this->assertSame(
$expected,
$request->getUri()->getQuery(),
);
return new Response();
});
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
query: $query,
);
$execute = new ReflectionMethod($instance, 'execute');
$execute->invoke($instance, $command);
}
/**
* Data provider for query parameter tests.
* @return array
*/
public static function query_provider(): array {
return [
'no query' => [
'query' => [],
'expected' => '',
],
'single query' => [
'query' => [
'name' => 'matrix',
],
'expected' => 'name=matrix',
],
'multiple queries' => [
'query' => [
'name' => 'matrix',
'type' => 'room',
],
'expected' => 'name=matrix&type=room',
],
];
}
/**
* Test the sendasjson constructor parameter.
*
* @dataProvider sendasjson_provider
* @param bool $sendasjson
* @param string $endpoint
* @param array $params
* @param array $remainingparams
* @param array $allparams
* @param array $expectedoptions
*/
public function test_send_as_json(
bool $sendasjson,
string $endpoint,
array $params,
array $remainingparams,
array $allparams,
array $expectedoptions,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $endpoint,
params: $params,
sendasjson: $sendasjson,
);
$this->assertSame($remainingparams, $command->get_remaining_params());
$this->assertSame($allparams, $command->get_all_params());
$this->assertSame($expectedoptions, $command->get_options());
}
/**
* Test the sendasjosn option to the command constructor.
*
* @return array
*/
public static function sendasjson_provider(): array {
return [
'As JSON' => [
'sendasjon' => true,
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'expectedoptions' => [
'json' => [
'name' => 'matrix',
],
],
],
'Not as JSON' => [
'sendasjson' => false,
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'expectedoptions' => [
],
],
];
}
/**
* Test the sendasjosn option to the command constructor.
*/
public function test_ignorehttperrors(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
ignorehttperrors: true,
);
$options = $command->get_options();
$this->assertArrayHasKey('http_errors', $options);
$this->assertFalse($options['http_errors']);
}
}
@@ -0,0 +1,458 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
use communication_matrix\local\command;
use communication_matrix\local\spec\v1p7;
use communication_matrix\local\spec\features;
use communication_matrix\tests\fixtures\mocked_matrix_client;
use core\http_client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/matrix_client_test_trait.php');
/**
* Tests for the matrix_client class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\matrix_client
* @coversDefaultClass \communication_matrix\matrix_client
*/
class matrix_client_test extends \advanced_testcase {
use matrix_client_test_trait;
/**
* Data provider for valid calls to ::instance.
* @return array
*/
public static function instance_provider(): array {
$testcases = [
'Standard versions' => [
null,
v1p7::class,
],
];
// Remove a couple of versions.
$versions = self::get_current_versions();
array_pop($versions);
array_pop($versions);
$testcases['Older server'] = [
$versions,
array_key_last($versions),
];
// Limited version compatibility, including newer than we support now.
$testcases['Newer versions with crossover'] = [
[
'v1.6',
'v1.7',
'v7.9',
],
\communication_matrix\local\spec\v1p7::class,
];
return $testcases;
}
/**
* Test that the instance method returns a valid instance for the given versions.
*
* @dataProvider instance_provider
* @param array|null $versions
* @param string $expectedversion
*/
public function test_instance(
?array $versions,
string $expectedversion,
): void {
// Create a mock and queue two responses.
$mock = new MockHandler([
$this->get_mocked_version_response($versions),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$instance = mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
$this->assertInstanceOf(matrix_client::class, $instance);
// Only the version API has been called.
$this->assertCount(1, $container);
$request = reset($container);
$this->assertEquals('/_matrix/client/versions', $request['request']->getUri()->getPath());
// The client should be a v1p7 client as that is the highest compatible version.
$this->assertInstanceOf($expectedversion, $instance);
}
/**
* Test that the instance method returns a valid instance for the given versions.
*/
public function test_instance_cached(): void {
$mock = new MockHandler([
$this->get_mocked_version_response(),
$this->get_mocked_version_response(),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$this->assertInstanceOf(matrix_client::class, $instance);
// Only the version API has been called.
$this->assertCount(1, $container);
// Call the API again. It should not lead to additional fetches.
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$this->assertCount(1, $container);
// But a different endpoint will.
$instance = mocked_matrix_client::instance('https://example.org', 'testtoken');
$this->assertCount(2, $container);
}
/**
* Test that the instance method throws an appropriate exception if no support is found.
*/
public function test_instance_no_support(): void {
// Create a mock and queue two responses.
$mock = new MockHandler([
$this->get_mocked_version_response(['v99.9']),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$this->expectException(moodle_exception::class);
$this->expectExceptionMessage('No supported Matrix API versions found.');
mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
}
/**
* Test the feature implementation check methods.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @dataProvider implements_feature_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_implements_feature(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expected, $instance->implements_feature($features));
}
/**
* Test the feature implementation requirement methods.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @covers ::require_feature
* @dataProvider implements_feature_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_require_feature(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->require_feature($features));
} else {
$this->expectException('moodle_exception');
$instance->require_feature($features);
}
}
/**
* Test the feature implementation requirement methods for a require all.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @covers ::require_feature
* @covers ::require_features
* @dataProvider require_features_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_require_features(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->require_features($features));
} else {
$this->expectException('moodle_exception');
$instance->require_features($features);
}
}
/**
* Data provider for feature implementation check tests.
*
* @return array
*/
public static function implements_feature_provider(): array {
return [
'Basic supported feature' => [
'v1.7',
features\matrix\media_create_v1::class,
true,
],
'Basic unsupported feature' => [
'v1.6',
features\matrix\media_create_v1::class,
false,
],
'[supported] as array' => [
'v1.6',
[features\matrix\create_room_v3::class],
true,
],
'[supported, supported] as array' => [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\update_room_avatar_v3::class,
],
true,
],
'[unsupported] as array' => [
'v1.6',
[
features\matrix\media_create_v1::class,
],
false,
],
'[unsupported, supported] as array' => [
'v1.6',
[
features\matrix\media_create_v1::class,
features\matrix\update_room_avatar_v3::class,
],
true,
],
];
}
/**
* Data provider for feature implementation check tests.
*
* @return array
*/
public static function require_features_provider(): array {
// We'll just add to the standard testcases.
$testcases = array_map(static function (array $testcase): array {
$testcase[1] = [$testcase[1]];
return $testcase;
}, self::implements_feature_provider());
$testcases['Require many supported features'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\update_room_avatar_v3::class,
],
true,
];
$testcases['Require many including an unsupported feature'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\media_create_v1::class,
],
false,
];
$testcases['Require many including an unsupported feature which has an alternate'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
[
features\matrix\media_create_v1::class,
features\matrix\update_room_avatar_v3::class,
],
],
true,
];
return $testcases;
}
/**
* Test the get_version method.
*
* @param string $version
* @param string $expectedversion
* @dataProvider get_version_provider
* @covers ::get_version
* @covers ::get_version_from_classname
*/
public function test_get_version(
string $version,
string $expectedversion,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expectedversion, $instance->get_version());
}
/**
* Data provider for get_version tests.
*
* @return array
*/
public static function get_version_provider(): array {
return [
['v1.1', '1.1'],
['v1.7', '1.7'],
];
}
/**
* Tests the meets_version method.
*
* @param string $version The version of the API to test against
* @param string $testversion The version to test
* @param bool $expected Whether the version meets the requirement
* @dataProvider meets_version_provider
* @covers ::meets_version
*/
public function test_meets_version(
string $version,
string $testversion,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expected, $instance->meets_version($testversion));
}
/**
* Tests the requires_version method.
*
* @param string $version The version of the API to test against
* @param string $testversion The version to test
* @param bool $expected Whether the version meets the requirement
* @dataProvider meets_version_provider
* @covers ::requires_version
*/
public function test_requires_version(
string $version,
string $testversion,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->requires_version($testversion));
} else {
$this->expectException('moodle_exception');
$instance->requires_version($testversion);
}
}
/**
* Data provider for meets_version tests.
*
* @return array
*/
public static function meets_version_provider(): array {
return [
'Same version' => ['v1.1', '1.1', true],
'Same version latest' => ['v1.7', '1.7', true],
'Newer version rejected' => ['v1.1', '1.7', false],
'Older version accepted' => ['v1.7', '1.1', true],
];
}
/**
* Test the execute method with a command.
*
* @covers ::execute
*/
public function test_command_is_executed(): void {
$historycontainer = [];
$mock = new MockHandler();
$instance = $this->get_mocked_instance_for_version('v1.6', $historycontainer, $mock);
$command = new command(
$instance,
method: 'GET',
endpoint: 'test/endpoint',
params: [
'test' => 'test',
],
);
$mock->append(new Response(200));
$rc = new \ReflectionClass($instance);
$rcm = $rc->getMethod('execute');
$result = $rcm->invoke($instance, $command);
$this->assertEquals(200, $result->getStatusCode());
$this->assertCount(1, $historycontainer);
$request = array_shift($historycontainer);
$this->assertEquals('GET', $request['request']->getMethod());
$this->assertEquals('/test/endpoint', $request['request']->getUri()->getPath());
}
}
@@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
use communication_matrix\local\spec\{v1p1, v1p2, v1p3, v1p4, v1p5, v1p6, v1p7};
use communication_matrix\tests\fixtures\mocked_matrix_client;
use core\http_client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
/**
* A trait with shared tooling for handling matrix_client tests.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait matrix_client_test_trait {
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
// Ensure that the mocked client is available.
require_once(__DIR__ . '/fixtures/mocked_matrix_client.php');
}
public function setUp(): void {
parent::setUp();
// Reset the test client.
mocked_matrix_client::reset_client();
}
public function tearDown(): void {
parent::tearDown();
// Reset the test client.
mocked_matrix_client::reset_client();
}
/**
* Get a mocked instance for a specific Matrix API version,
*
* @param string $version
* @param array $historycontainer An array which will be filled with history for the mocked client.
* @param MockHandler|null $mock A MockHandler object that can be appended to
* @return matrix_client
*/
protected function get_mocked_instance_for_version(
string $version,
array &$historycontainer = [],
?MockHandler $mock = null,
): matrix_client {
if ($mock === null) {
$mock = new MockHandler();
}
// Add the version response.
$mock->append($this->get_mocked_version_response([$version]));
$handlerstack = HandlerStack::create($mock);
$history = Middleware::history($historycontainer);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$client = mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
// Remove the request that is required to fetch the version from the history.
array_shift($historycontainer);
return $client;
}
/**
* Get a mocked response for the /versions well-known URI.
*
* @param array|null $versions
* @param array|null $unstablefeatures
* @return Response
*/
protected function get_mocked_version_response(
array $versions = null,
array $unstablefeatures = null,
): Response {
$data = (object) [
"versions" => array_values(self::get_current_versions()),
"unstable_features" => self::get_current_unstable_features(),
];
if ($versions) {
$data->versions = array_values($versions);
}
if ($unstablefeatures) {
$data->unstable_features = $unstablefeatures;
}
return new Response(200, [], json_encode($data));
}
/**
* A helper to get the current versions returned by synapse.
*
* @return array
*/
protected static function get_current_versions(): array {
return [
v1p1::class => "v1.1",
v1p2::class => "v1.2",
v1p3::class => "v1.3",
v1p4::class => "v1.4",
v1p5::class => "v1.5",
v1p6::class => "v1.6",
v1p7::class => "v1.7",
];
}
/**
* A helper to get the current unstable features returned by synapse.
* @return array
*/
protected static function get_current_unstable_features(): array {
return [
"org.matrix.label_based_filtering" => true,
"org.matrix.e2e_cross_signing" => true,
"org.matrix.msc2432" => true,
"uk.half-shot.msc2666.query_mutual_rooms" => true,
"io.element.e2ee_forced.public" => false,
"io.element.e2ee_forced.private" => false,
"io.element.e2ee_forced.trusted_private" => false,
"org.matrix.msc3026.busy_presence" => false,
"org.matrix.msc2285.stable" => true,
"org.matrix.msc3827.stable" => true,
"org.matrix.msc2716" => false,
"org.matrix.msc3440.stable" => true,
"org.matrix.msc3771" => true,
"org.matrix.msc3773" => false,
"fi.mau.msc2815" => false,
"fi.mau.msc2659.stable" => true,
"org.matrix.msc3882" => false,
"org.matrix.msc3881" => false,
"org.matrix.msc3874" => false,
"org.matrix.msc3886" => false,
"org.matrix.msc3912" => false,
"org.matrix.msc3952_intentional_mentions" => false,
"org.matrix.msc3981" => false,
"org.matrix.msc3391" => false,
];
}
}
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
/**
* Tests for the matrix_room class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \communication_matrix\matrix_room
*/
class matrix_room_test extends \advanced_testcase {
/**
* Test for load_by_processor_id with no record.
*
* @covers ::load_by_processor_id
*/
public function test_load_by_processor_id_none(): void {
$this->assertNull(matrix_room::load_by_processor_id(999999999));
}
/**
* Test for load_by_processor_id with valid records.
*
* @covers ::create_room_record
* @covers ::__construct
* @covers ::load_by_processor_id
* @covers ::get_processor_id
* @covers ::get_room_id
* @covers ::get_topic
*/
public function test_create_room_record(): void {
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 10000,
topic: null,
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(10000, $room->get_processor_id());
$this->assertNotNull('', $room->get_topic());
$this->assertEquals('', $room->get_topic());
$this->assertNull($room->get_room_id());
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is thusly',
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(12345, $room->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $room->get_topic());
$this->assertNull($room->get_room_id());
$room = matrix_room::create_room_record(
processorid: 54321,
topic: 'The topic of this room is thusly',
roomid: 'This is a roomid',
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(54321, $room->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $room->get_topic());
$this->assertEquals('This is a roomid', $room->get_room_id());
$reloadedroom = matrix_room::load_by_processor_id(54321);
$this->assertEquals(54321, $reloadedroom->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $reloadedroom->get_topic());
$this->assertEquals('This is a roomid', $reloadedroom->get_room_id());
}
/**
* Test for update_room_record.
*
* @covers ::update_room_record
*/
public function test_update_room_record(): void {
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is that',
);
// Add a roomid.
$room->update_room_record(
roomid: 'This is a roomid',
);
$this->assertEquals('This is a roomid', $room->get_room_id());
$this->assertEquals('The topic of this room is that', $room->get_topic());
$this->assertEquals(12345, $room->get_processor_id());
// Alter the roomid and topic.
$room->update_room_record(
roomid: 'updatedRoomId',
topic: 'updatedTopic is here',
);
$this->assertEquals('updatedRoomId', $room->get_room_id());
$this->assertEquals('updatedTopic is here', $room->get_topic());
$this->assertEquals(12345, $room->get_processor_id());
}
/**
* Tests for delete_room_record.
*
* @covers ::delete_room_record
*/
public function test_delete_room_record(): void {
global $DB;
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is that',
);
$this->assertCount(1, $DB->get_records('matrix_room'));
$room->delete_room_record();
$this->assertCount(0, $DB->get_records('matrix_room'));
}
}
@@ -0,0 +1,295 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
use core\context;
use GuzzleHttp\Psr7\Response;
/**
* Trait matrix_helper_trait to generate initial setup for matrix mock and associated helpers.
*
* @package communication_matrix
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait matrix_test_helper_trait {
/**
* @var string $accesstoken The token for matrix connection
*/
protected string $accesstoken;
/**
* @var string $matrixhomeserverurl The server url of matrix synapse server
*/
protected string $matrixhomeserverurl;
/**
* Initialize the mock configs in settings.
*
* @return void
*/
protected function initialise_mock_configs(): void {
$this->matrixhomeserverurl = TEST_COMMUNICATION_MATRIX_MOCK_SERVER;
set_config('matrixhomeserverurl', $this->matrixhomeserverurl, 'communication_matrix');
$request = $this->request();
$response = $request->post($this->matrixhomeserverurl . '/backoffice/create-admin');
$admindata = json_decode($response->getBody());
$json = [
'identifier' => [
'type' => 'm.id.user',
'user' => $admindata->user_id,
],
'type' => 'm.login.password',
'password' => $admindata->password,
];
$request = $this->request($json);
$response = $request->post($this->matrixhomeserverurl . '/_matrix/client/r0/login');
$response = json_decode($response->getBody());
if (empty($response->access_token)) {
$this->markTestSkipped(
'The matrix mock server is not responsive, can not continue the tests'
);
}
$this->accesstoken = $response->access_token;
set_config('matrixaccesstoken', $this->accesstoken, 'communication_matrix');
}
/**
* Get the mock server url.
*
* @return string
*/
public function get_matrix_server_url(): string {
if (empty($this->matrixhomeserverurl)) {
throw new \coding_exception('Can not get this information without initializing the mock server.');
}
return $this->matrixhomeserverurl;
}
/**
* Get the matrix access token.
*
* @return string
*/
public function get_matrix_access_token(): string {
if (empty($this->accesstoken)) {
throw new \coding_exception('Can not get this information without initializing the mock server.');
}
return $this->accesstoken;
}
/**
* This test requires mock server to be present.
*
* @return void
*/
protected function initialise_mock_server(): void {
if (!defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
$this->markTestSkipped(
'The TEST_COMMUNICATION_MATRIX_MOCK_SERVER constant must be defined to run communication_matrix tests'
);
}
$this->reset_mock();
$this->initialise_mock_configs();
}
/**
* Get matrix room data from matrix server.
*
* @param string $roomid The id of the room
* @return \stdClass
*/
public function get_matrix_room_data(string $roomid): \stdClass {
$rooms = $this->backoffice_get_all_rooms();
foreach ($rooms as $room) {
if ($room->room_id === $roomid) {
return $room;
}
}
}
/**
* Get matrix user data from matrix server.
*
* @param string $roomid The id of the room
* @param string $matrixuserid The id of the user
* @return \stdClass
*/
public function get_matrix_user_data(string $roomid, string $matrixuserid): \stdClass {
$users = $this->backoffice_get_all_users();
foreach ($users as $user) {
if ($user->userid === $matrixuserid) {
return $user;
}
}
}
/**
* A backoffice call to get all registered users from our mock server.
*
* @return array
*/
public function backoffice_get_all_users(): array {
$client = new \core\http_client();
return json_decode($client->get($this->get_backoffice_uri('users'))->getBody())->users;
}
/**
* A backoffice method to create users and rooms on our mock server.
*
* @param array $users
* @param array $rooms
*/
public function backoffice_create_users_and_rooms(
array $users = [],
array $rooms = [],
): Response {
$client = new \core\http_client();
return $client->put(
$this->get_backoffice_uri('create'),
[
'json' => [
'users' => $users,
'rooms' => $rooms,
],
],
);
}
/**
* The http request for the api call.
*
* @param array $jsonarray The array of json
* @param array $headers The array of headers
* @return \core\http_client
*/
public function request(array $jsonarray = [], array $headers = []): \core\http_client {
$response = new \core\http_client([
'headers' => $headers,
'json' => $jsonarray,
]);
return $response;
}
/**
* Get the URI of a backoffice endpoint on the mock server.
*
* @param string $endpoint
* @return string
*/
protected function get_backoffice_uri(string $endpoint): string {
return $this->get_matrix_server_url() . '/backoffice/' . $endpoint;
}
/**
* Fetch all rooms from the back office.
*
* @return array
*/
public function backoffice_get_all_rooms(): array {
$client = new \core\http_client();
return json_decode($client->get($this->get_backoffice_uri('rooms'))->getBody())->rooms;
}
/**
* Return the first room from the server.
*
* In most cases there is only one room.
* @return \stdClass
*/
public function backoffice_get_room(): \stdClass {
// Fetch the room information from the server.
$rooms = $this->backoffice_get_all_rooms();
$this->assertCount(1, $rooms);
$room = reset($rooms);
return $room;
}
/**
* Reset the mock server
*
* @return void
*/
public function reset_mock(): void {
if (defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
$request = $this->request();
$response = $request->post(TEST_COMMUNICATION_MATRIX_MOCK_SERVER . '/backoffice/reset');
$response = json_decode($response->getBody());
if (empty($response->reset)) {
$this->markTestSkipped(
'The matrix mock server is not responsive, can not continue the tests'
);
}
}
}
/**
* Helper to create a room.
*
* @param null|string $component
* @param null|string $itemtype
* @param null|int $itemid
* @param null|string $roomname
* @param null|string $roomtopic
* @param null|stored_file $roomavatar
* @param array $members
* @return api
*/
protected function create_matrix_room(
?string $component = 'core_course',
?string $itemtype = 'example',
?int $itemid = 1,
?string $roomname = null,
?string $roomtopic = null,
?\stored_file $roomavatar = null,
array $members = [],
?context $context = null,
): \core_communication\api {
$context = $context ?? \core\context\system::instance();
// Create a new room.
$communication = \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: $roomname ?? 'Room name',
avatar: $roomavatar,
instance: (object) [
'matrixroomtopic' => $roomtopic ?? 'A fun topic',
],
);
$communication->add_members_to_room($members);
// Run the adhoc task.
$this->run_all_adhoc_tasks();
return \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
);
}
}
@@ -0,0 +1,239 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
use moodle_exception;
/**
* Class matrix_user_manager_test to test the matrix user manager.
*
* @package communication_matrix
* @category test
* @copyright 2023 Stevani Andolo <stevani.andolo@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\matrix_user_manager
*/
class matrix_user_manager_test extends \advanced_testcase {
/**
* Test fetcihing a users matrix userid from Moodle.
*/
public function test_get_matrixid_from_moodle_without_field(): void {
$user = get_admin();
$this->assertNull(matrix_user_manager::get_matrixid_from_moodle($user->id));
}
/**
* Test fetching a user's matrix userid from Moodle.
*/
public function test_get_matrixid_from_moodle(): void {
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Add user ids to both users.
matrix_user_manager::set_matrix_userid_in_moodle(
$user1->id,
'@someexampleuser:matrix.moodle.org',
);
matrix_user_manager::set_matrix_userid_in_moodle(
$user2->id,
'@someotherexampleuser:matrix.moodle.org',
);
// And confirm that they're fetched back.
$this->assertEquals(
'@someexampleuser:matrix.moodle.org',
matrix_user_manager::get_matrixid_from_moodle($user1->id),
);
$this->assertEquals(
'@someotherexampleuser:matrix.moodle.org',
matrix_user_manager::get_matrixid_from_moodle($user2->id),
);
}
/**
* Test fetching a formatted matrix userid from Moodle when no server is set.
*/
public function test_get_formatted_matrix_userid_unset(): void {
$this->expectException(moodle_exception::class);
matrix_user_manager::get_formatted_matrix_userid('No value');
}
/**
* Test fetch of a formatted matrix userid.
*
* @dataProvider get_formatted_matrix_userid_provider
* @param string $server
* @param string $username The moodle username to turn into a Matrix username
* @param string $expecteduserid The expected matrix user id
*/
public function test_get_formatted_matrix_userid(
string $server,
string $username,
string $expecteduserid,
): void {
$this->resetAfterTest();
set_config('matrixhomeserverurl', $server, 'communication_matrix');
$this->assertEquals(
$expecteduserid,
matrix_user_manager::get_formatted_matrix_userid($username),
);
}
/**
* Data provider for get_formatted_matrix_userid.
*
* @return array
*/
public static function get_formatted_matrix_userid_provider(): array {
return [
'alphanumeric' => [
'https://matrix.example.org',
'alphabet1',
'@alphabet1:matrix.example.org',
],
'chara' => [
'https://matrix.example.org',
'asdf#$%^&*()+{}|<>?!,asdf',
'@asdf.................asdf:matrix.example.org',
],
'local server' => [
'https://synapse',
'colin.creavey',
'@colin.creavey:synapse',
],
'server with port' => [
'https://matrix.example.org:8448',
'colin.creavey',
'@colin.creavey:matrix.example.org',
],
'numeric username' => [
'https://matrix.example.org',
'123456',
'@' . matrix_user_manager::MATRIX_USER_PREFIX . '123456:matrix.example.org',
],
];
}
/**
* Data provider for set_matrix_userid_in_moodle.
*
* @return array
*/
public static function set_matrix_userid_in_moodle_provider(): array {
return array_combine(
array_keys(self::get_formatted_matrix_userid_provider()),
array_map(
fn($value) => [$value[2]],
self::get_formatted_matrix_userid_provider(),
),
);
}
/**
* Test setting of a user's matrix userid in Moodle.
*
* @dataProvider set_matrix_userid_in_moodle_provider
* @param string $expectedusername
*/
public function test_set_matrix_userid_in_moodle(
string $expectedusername,
): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
matrix_user_manager::set_matrix_userid_in_moodle($user->id, $expectedusername);
// Get created matrixuserid from moodle.
$this->assertEquals(
$expectedusername,
matrix_user_manager::get_matrixid_from_moodle($user->id),
);
}
/**
* Test for getting a formatted matrix home server id.
*
* @dataProvider get_formatted_matrix_home_server_provider
* @param string $input
* @param string $expectedoutput
*/
public function test_get_formatted_matrix_home_server(
string $input,
string $expectedoutput
): void {
$this->resetAfterTest();
set_config(
'matrixhomeserverurl',
$input,
'communication_matrix',
);
$this->assertEquals(
$expectedoutput,
matrix_user_manager::get_formatted_matrix_home_server(),
);
}
/**
* Data provider for get_formatted_matrix_home_server.
*
* @return array
*/
public static function get_formatted_matrix_home_server_provider(): array {
return [
'www is removed' => [
'https://www.example.org',
'example.org',
],
'www is not removed if it is not at the beginning' => [
'https://matrix.www.example.org',
'matrix.www.example.org',
],
'others are not removed' => [
'https://matrix.example.org',
'matrix.example.org',
],
];
}
/**
* Test creation of matrix user profile fields.
*/
public function test_create_matrix_user_profile_fields(): void {
global $CFG;
require_once("{$CFG->dirroot}/user/profile/lib.php");
$this->resetAfterTest();
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
$this->assertFalse($matrixprofilefield);
$this->assertIsString(matrix_user_manager::create_matrix_user_profile_fields());
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
$this->assertNotFalse($matrixprofilefield);
$user = $this->getDataGenerator()->create_user();
$this->assertObjectHasProperty($matrixprofilefield, profile_user_record($user->id));
}
}