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
+132
View File
@@ -0,0 +1,132 @@
<?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 core_xapi;
use core_xapi\local\statement\item_activity;
use advanced_testcase;
/**
* Contains test cases for testing xAPI API base methods.
*
* @package core_xapi
* @since Moodle 4.2
* @covers \core_xapi\api
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Testing remove_states_from_component method.
*
* @return void
*/
public function test_remove_states_from_component(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'unexisting'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('9'), 'component' => 'unexisting'], true);
// Check no state has been removed (because there are no entries for the another_component).
api::remove_states_from_component('another_component');
$this->assertEquals(9, $DB->count_records('xapi_states'));
// Check states for the fake_component have been removed.
api::remove_states_from_component('fake_component');
$this->assertEquals(5, $DB->count_records('xapi_states'));
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'fake_component']));
$this->assertEquals(3, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
$this->assertEquals(2, $DB->count_records('xapi_states', ['component' => 'unexisting']));
// Check states for the mod_h5pactivity have been removed too.
api::remove_states_from_component('mod_h5pactivity');
$this->assertEquals(2, $DB->count_records('xapi_states'));
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
// Check states for the unexisting component have been removed (using the default state_store).
api::remove_states_from_component('unexisting');
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
/**
* Testing execute_state_cleanup method.
*
* @return void
*/
public function test_execute_state_cleanup(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
// Perform test.
api::execute_state_cleanup();
// Check no state has been removed yet (because the entries are not old enough).
$this->assertEquals(7, $DB->count_records('xapi_states'));
// Make the existing state entries older.
$timepast = time() - 2;
$DB->set_field('xapi_states', 'timecreated', $timepast);
$DB->set_field('xapi_states', 'timemodified', $timepast);
// Create 1 more state, that shouldn't be removed after the cleanup.
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'mod_h5pactivity'], true);
// Set the config to remove states older than 1 second.
set_config('xapicleanupperiod', 1);
// Check old states have been removed.
api::execute_state_cleanup();
$this->assertEquals(5, $DB->count_records('xapi_states'));
$this->assertEquals(4, $DB->count_records('xapi_states', ['component' => 'fake_component']));
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'my_component']));
}
}
+241
View File
@@ -0,0 +1,241 @@
<?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 core_xapi\external;
use core_xapi\xapi_exception;
use core_xapi\local\statement\item_agent;
use externallib_advanced_testcase;
use core_external\external_api;
use core_xapi\iri;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI delete state webservice.
*
* @package core_xapi
* @covers \core_xapi\external\delete_state
* @since Moodle 4.2
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_state_test extends externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
}
/**
* Testing different component names on valid states.
*
* @dataProvider components_provider
* @param string $component component name
* @param string|null $expected expected results
*/
public function test_component_names(string $component, ?string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$data = test_helper::create_state([], true);
$this->delete_state_data($component, $data, $expected);
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'component' => 'inexistent_component',
'expected' => null,
],
'Compatible component' => [
'component' => 'fake_component',
'expected' => 'true',
],
'Incompatible component' => [
'component' => 'core_xapi',
'expected' => null,
],
];
}
/**
* Testing invalid agent.
*
*/
public function test_invalid_agent(): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Invalid agent (use different user, instead of the current one).
$info = [
'agent' => item_agent::create_from_user($other),
];
$data = test_helper::create_state($info, true);
$this->delete_state_data('fake_component', $data, null);
}
/**
* Testing valid/invalid state.
*
* @dataProvider states_provider
* @param array $info array of overriden state data.
* @param string|null $expected Expected results.
* @return void
*/
public function test_delete_state(array $info, ?string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = $info['component'] ?? 'fake_component';
$params = [];
if ($component === 'mod_h5pactivity') {
// For the mod_h5pactivity component, the activity needs to be created too.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = \context_module::instance($activity->cmid);
$info['activity'] = item_activity::create_from_id($activitycontext->id);
$params['activity'] = $info['activity'];
$this->setUser($user);
}
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state($params, true);
// Perform test.
$data = test_helper::create_state($info);
$this->delete_state_data($component, $data, $expected);
}
/**
* Data provider for the test_get_state tests.
*
* @return array
*/
public function states_provider(): array {
return [
'Existing and valid state' => [
'info' => [],
'expected' => 'true',
],
'No state (wrong activityid)' => [
'info' => ['activity' => item_activity::create_from_id('1')],
'expected' => 'false',
],
'No state (wrong stateid)' => [
'info' => ['stateid' => 'food'],
'expected' => 'false',
],
'No state (wrong component)' => [
'info' => ['component' => 'mod_h5pactivity'],
'expected' => 'false',
],
];
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return delete_state the external class
*/
private function get_external_class(): delete_state {
$ws = new class extends delete_state {
/**
* Method to override validate_component.
*
* @param string $component The component name in frankenstyle.
*/
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* This function do all checks from a standard delete_state request.
*
* The reason for this function is because states crafting (special in error
* scenarios) is complicated to do via data providers because every test need a specific
* testing conditions. For this reason alls tests creates a scenario and then uses this
* function to check the results.
*
* @param string $component component name
* @param state $data data to encode and send to delete_state
* @param string $expected expected results (if null an exception is expected)
*/
private function delete_state_data(string $component, state $data, ?string $expected): void {
global $DB;
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// When no result is expected, an exception will be incurred.
if (is_null($expected)) {
$this->expectException(xapi_exception::class);
}
$external = $this->get_external_class();
$result = $external::execute(
$component,
iri::generate($data->get_activity_id(), 'activity'),
json_encode($data->get_agent()),
$data->get_state_id(),
$data->get_registration()
);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
// Check the state has been removed.
$records = $DB->get_records('xapi_states');
$this->assertTrue($result);
if ($expected === 'true') {
$this->assertCount($currentstates - 1, $records);
} else if ($expected === 'false') {
$this->assertCount($currentstates, $records);
}
}
}
+325
View File
@@ -0,0 +1,325 @@
<?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 core_xapi\external;
use core\context\module;
use core_external\external_api;
use core_xapi\iri;
use core_xapi\local\statement\item_activity;
use core_xapi\local\statement\item_agent;
use core_xapi\test_helper;
use core_xapi\xapi_exception;
use externallib_advanced_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI delete states webservice.
*
* @package core_xapi
* @covers \core_xapi\external\delete_states
* @since Moodle 4.3
* @copyright 2023 Laurent David <laurent.david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete_states_test extends externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
}
/**
* Testing different component names on valid states.
*
* @dataProvider components_provider
* @param string $component component name
* @param object|null $expected expected results
*/
public function test_component_names(string $component, ?object $expected): void {
global $DB, $USER;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$info = [
'agent' => item_agent::create_from_user($USER),
'activity' => item_activity::create_from_id('12345'),
];
test_helper::create_state($info, true);
if (!empty($expected->exception)) {
$this->expectException($expected->exception);
}
$this->execute($component,
iri::generate($info['activity']->get_id(), 'activity'),
json_encode($info['agent'])
);
if (isset($expected->expectedcount)) {
$this->assertEquals($expected->expectedcount, $DB->record_exists('xapi_states', []));
}
}
/**
* This function execute the delete_states_data
*
* @param string $component component name
* @param string $activityiri
* @param string $agent
* @param string|null $registration
* @return array empty array
*/
private function execute(string $component,
string $activityiri,
string $agent,
?string $registration = null
): void {
$external = $this->get_external_class();
$external::execute(
$component,
$activityiri,
$agent,
$registration
);
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return delete_states the external class
*/
private function get_external_class(): delete_states {
$ws = new class extends delete_states {
/**
* Method to override validate_component.
*
* @param string $component The component name in frankenstyle.
*/
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'component' => 'inexistent_component',
'expected' => (object) ['exception' => xapi_exception::class],
],
'Compatible component' => [
'component' => 'fake_component',
'expected' => (object) ['expectedcount' => 0],
],
'Incompatible component' => [
'component' => 'core_xapi',
'expected' => (object) ['exception' => xapi_exception::class],
],
];
}
/**
* Testing invalid agent.
*
*/
public function test_invalid_agent(): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Invalid agent (use different user, instead of the current one).
$info = [
'agent' => item_agent::create_from_user($other),
'activity' => item_activity::create_from_id('12345'),
];
test_helper::create_state($info, true);
$this->expectException(xapi_exception::class);
$this->execute(
'fake_component',
iri::generate($info['activity']->get_id(), 'activity'),
json_encode($info['agent'])
);
}
/**
* Testing deleting states
*
* @dataProvider states_provider
* @param string $testedusername
* @param string $testedcomponent
* @param string $testedactivityname
* @param array $states
* @param array $expectedstates
* @return void
*/
public function test_delete_states(string $testedusername,
string $testedcomponent,
string $testedactivityname,
array $states,
array $expectedstates
): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activities = [];
$users = [];
// Create a set of states for different users and components.
foreach ($states as $stateinfo) {
$params = [
'component' => $stateinfo['component'] ?? 'mod_h5pactivity',
];
$uname = $stateinfo['user'];
$user = $users[$uname] ?? null;
if (empty($user)) {
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$users[$uname] = $user;
}
$activityname = $stateinfo['activity'];
$activity = $activities[$activityname] ?? null;
if (empty($activity)) {
if (empty($stateinfo['activityid'])) {
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = module::instance($activity->cmid);
$activities[$activityname] = item_activity::create_from_id($activitycontext->id);
} else {
$activities[$activityname] = item_activity::create_from_id($stateinfo['activityid']);
}
}
$params['activity'] = $activities[$activityname];
$params['agent'] = item_agent::create_from_user($user);
test_helper::create_state($params, true);
}
if (empty($users[$testedusername])) {
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$users[$testedusername] = $user;
}
$this->setUser($users[$testedusername]);
$activity = $activities[$testedactivityname];
$activityiri = iri::generate($activity->get_id(), 'activity');
$agent = json_encode(item_agent::create_from_user($users[$testedusername]));
$this->execute($testedcomponent,
$activityiri,
$agent);
$statesleft = $DB->get_records('xapi_states');
// Check that we have the expected leftover records.
$this->assertCount(count($expectedstates), $statesleft);
foreach ($expectedstates as $expectedstate) {
$expectedactivityid = $activities[$expectedstate['activity']]->get_id();
$expecteduserid = $users[$expectedstate['user']]->id;
$found = false;
foreach ($statesleft as $state) {
if ($state->userid == $expecteduserid && $state->itemid == $expectedactivityid) {
$found = true;
break;
}
}
$this->assertTrue($found, 'State not found:' . json_encode($statesleft));
}
}
/**
* Data provider for the test_get_state tests.
*
* @return array
*/
public function states_provider(): array {
return [
'Activities with different users and components' => [
'username' => 'user1',
'component' => 'mod_h5pactivity',
'activity' => 'Activity 1',
'states' => [
[
'user' => 'user1',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
[
'user' => 'user2',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
[
'user' => 'user1',
'activity' => 'Activity 3',
'activityid' => '1',
'component' => 'core_xapi'
],
[
'user' => 'user1',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
],
'expectedstatesleft' => [
['user' => 'user2', 'activity' => 'Activity 1'],
['user' => 'user1', 'activity' => 'Activity 3']
]
],
'Activities with one single user' => [
'username' => 'user1',
'component' => 'mod_h5pactivity',
'activity' => 'Activity 1',
'states' => [
[
'user' => 'user1',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
[
'user' => 'user1',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
[
'user' => 'user1',
'activity' => 'Activity 1',
'component' => 'mod_h5pactivity'
],
],
'expectedstatesleft' => []
],
];
}
}
+234
View File
@@ -0,0 +1,234 @@
<?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 core_xapi\external;
use core_xapi\xapi_exception;
use core_xapi\local\statement\item_agent;
use externallib_advanced_testcase;
use core_external\external_api;
use core_xapi\iri;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI get state webservice.
*
* @package core_xapi
* @covers \core_xapi\external\get_state
* @since Moodle 4.2
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_state_test extends externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
}
/**
* Testing different component names on valid states.
*
* @dataProvider components_provider
* @param string $component component name
* @param string|null $expected expected results
*/
public function test_component_names(string $component, ?string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add, at least, one xAPI state record to database.
$data = test_helper::create_state([], true);
// Perform test.
$this->get_state_data($component, $data, $expected);
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'component' => 'inexistent_component',
'expected' => null,
],
'Compatible component' => [
'component' => 'fake_component',
'expected' => 'true',
],
'Incompatible component' => [
'component' => 'core_xapi',
'expected' => null,
],
];
}
/**
* Testing invalid agent for get_state.
*
*/
public function test_invalid_agent(): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Invalid agent (use different user, instead of the current one).
$info = [
'agent' => item_agent::create_from_user($other),
];
$data = test_helper::create_state($info, true);
$this->get_state_data('fake_component', $data, null);
}
/**
* Testing valid/invalid state.
*
* @dataProvider states_provider
* @param array $info The xAPI state information (to override default values).
* @param string $expected Expected results.
*/
public function test_get_state(array $info, string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = $info['component'] ?? 'fake_component';
$params = [];
if ($component === 'mod_h5pactivity') {
// For the mod_h5pactivity component, the activity needs to be created too.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$activitycontext = \context_module::instance($activity->cmid);
$info['activity'] = item_activity::create_from_id($activitycontext->id);
$params['activity'] = $info['activity'];
$this->setUser($user);
}
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state($params, true);
// Perform test.
$data = test_helper::create_state($info);
$component = $info['component'] ?? 'fake_component';
$this->get_state_data($component, $data, $expected);
}
/**
* Data provider for the test_get_state tests.
*
* @return array
*/
public function states_provider(): array {
return [
'Existing and valid state' => [
'info' => [],
'expected' => 'true',
],
'No state (wrong activityid)' => [
'info' => ['activity' => item_activity::create_from_id('1')],
'expected' => 'false',
],
'No state (wrong stateid)' => [
'info' => ['stateid' => 'food'],
'expected' => 'false',
],
'No state (wrong component)' => [
'info' => ['component' => 'mod_h5pactivity'],
'expected' => 'false',
],
];
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return get_state the external class
*/
private function get_external_class(): get_state {
$ws = new class extends get_state {
/**
* Method to override validate_component.
*
* @param string $component The component name in frankenstyle.
*/
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* This function do all checks from a standard get_state request.
*
* The reason for this function is because states crafting (special in error
* scenarios) is complicated to do via data providers because every test need a specific
* testing conditions. For this reason alls tests creates a scenario and then uses this
* function to check the results.
*
* @param string $component component name
* @param state $data data to encode and send to get_state
* @param string $expected expected results (if null an exception is expected)
*/
private function get_state_data(string $component, state $data, ?string $expected): void {
// When no result is expected, an exception will be incurred.
if (is_null($expected)) {
$this->expectException(xapi_exception::class);
}
$external = $this->get_external_class();
$result = $external::execute(
$component,
iri::generate($data->get_activity_id(), 'activity'),
json_encode($data->get_agent()),
$data->get_state_id(),
$data->get_registration()
);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
// Check the returned state has the expected values.
if ($expected === 'true') {
$this->assertEquals(json_encode($data->jsonSerialize()), $result);
} else {
$this->assertNull($result);
}
}
}
+397
View File
@@ -0,0 +1,397 @@
<?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 core_xapi\external;
use core_xapi\xapi_exception;
use core_xapi\local\statement\item_agent;
use externallib_advanced_testcase;
use core_external\external_api;
use core_xapi\iri;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI get states webservice.
*
* @package core_xapi
* @covers \core_xapi\external\get_states
* @since Moodle 4.2
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_states_test extends externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
}
/**
* Execute the get_states service from a generate state.
*
* @param string $component component name
* @param state $data the original state to extract the params
* @param string|null $since the formated timestamp or ISO 8601 date
* @param array $override overridden params
* @return string[] array of state ids
*/
private function execute_service(
string $component,
state $data,
?string $since = null,
array $override = []
): array {
// Apply overrides.
$activityiri = $override['activityiri'] ?? iri::generate($data->get_activity_id(), 'activity');
$registration = $override['registration'] ?? $data->get_registration();
$agent = $override['agent'] ?? $data->get_agent();
if (!empty($override['user'])) {
$agent = item_agent::create_from_user($override['user']);
}
$external = $this->get_external_class();
$result = $external::execute(
$component,
$activityiri,
json_encode($agent),
$registration,
$since
);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
// Sorting result to make them comparable.
sort($result);
return $result;
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return get_states the external class
*/
private function get_external_class(): get_states {
$ws = new class extends get_states {
/**
* Method to override validate_component.
*
* @param string $component The component name in frankenstyle.
*/
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* Testing different component names on valid states.
*
* @dataProvider components_provider
* @param string $component component name
* @param string|null $exception expect exception
*/
public function test_component_names(string $component, ?bool $exception): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add, at least, one xAPI state record to database.
$data = test_helper::create_state(
['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
true
);
// If no result is expected we will just incur in exception.
if ($exception) {
$this->expectException(xapi_exception::class);
}
$result = $this->execute_service($component, $data);
$this->assertEquals(['aa'], $result);
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'component' => 'inexistent_component',
'exception' => true,
],
'Compatible component' => [
'component' => 'fake_component',
'exception' => false,
],
'Incompatible component' => [
'component' => 'core_xapi',
'exception' => true,
],
];
}
/**
* Testing different since date formats.
*
* @dataProvider since_formats_provider
* @param string|null $since the formatted timestamps
* @param string[]|null $expected expected results
* @param bool $exception expect exception
*/
public function test_since_formats(?string $since, ?array $expected, bool $exception = false): void {
$this->resetAfterTest();
$this->setAdminUser();
$states = $this->generate_states();
if ($exception) {
$this->expectException(xapi_exception::class);
}
$result = $this->execute_service('fake_component', $states['aa'], $since);
$this->assertEquals($expected, $result);
}
/**
* Data provider for the test_since_formats tests.
*
* @return array
*/
public function since_formats_provider(): array {
return [
'Null date' => [
'since' => null,
'expected' => ['aa', 'bb', 'cc', 'dd'],
'exception' => false,
],
'Numeric timestamp' => [
'since' => '1651100399',
'expected' => ['aa', 'bb'],
'exception' => false,
],
'ISO 8601 format 1' => [
'since' => '2022-04-28T06:59',
'expected' => ['aa', 'bb'],
'exception' => false,
],
'ISO 8601 format 2' => [
'since' => '2022-04-28T06:59:59',
'expected' => ['aa', 'bb'],
'exception' => false,
],
'Wrong format' => [
'since' => 'Spanish omelette without onion',
'expected' => null,
'exception' => true,
],
];
}
/**
* Testing different activity IRI values.
*
* @dataProvider activity_iri_provider
* @param string|null $activityiri
* @param string[]|null $expected expected results
*/
public function test_activity_iri(?string $activityiri, ?array $expected): void {
$this->resetAfterTest();
$this->setAdminUser();
$states = $this->generate_states();
$override = ['activityiri' => $activityiri];
$result = $this->execute_service('fake_component', $states['aa'], null, $override);
$this->assertEquals($expected, $result);
}
/**
* Data provider for the test_activity_iri tests.
*
* @return array
*/
public function activity_iri_provider(): array {
return [
'Activity with several states' => [
'activityiri' => iri::generate('1', 'activity'),
'expected' => ['aa', 'bb', 'cc', 'dd'],
],
'Activity with one state' => [
'activityiri' => iri::generate('2', 'activity'),
'expected' => ['ee'],
],
'Inexistent activity' => [
'activityiri' => iri::generate('3', 'activity'),
'expected' => [],
],
];
}
/**
* Testing different agent values.
*
* @dataProvider agent_values_provider
* @param string|null $agentreference the used agent reference
* @param string[]|null $expected expected results
* @param bool $exception expect exception
*/
public function test_agent_values(?string $agentreference, ?array $expected, bool $exception = false): void {
$this->resetAfterTest();
$this->setAdminUser();
$states = $this->generate_states();
if ($exception) {
$this->expectException(xapi_exception::class);
}
$userreferences = [
'current' => $states['aa']->get_user(),
'other' => $this->getDataGenerator()->create_user(),
];
$override = [
'user' => $userreferences[$agentreference],
];
$result = $this->execute_service('fake_component', $states['aa'], null, $override);
$this->assertEquals($expected, $result);
}
/**
* Data provider for the test_agent_values tests.
*
* @return array
*/
public function agent_values_provider(): array {
return [
'Current user' => [
'agentreference' => 'current',
'expected' => ['aa', 'bb', 'cc', 'dd'],
'exception' => false,
],
'Other user' => [
'agentreference' => 'other',
'expected' => null,
'exception' => true,
],
];
}
/**
* Testing different registration values.
*
* @dataProvider registration_values_provider
* @param string|null $registration
* @param string[]|null $expected expected results
*/
public function test_registration_values(?string $registration, ?array $expected): void {
$this->resetAfterTest();
$this->setAdminUser();
$states = $this->generate_states();
$override = ['registration' => $registration];
$result = $this->execute_service('fake_component', $states['aa'], null, $override);
$this->assertEquals($expected, $result);
}
/**
* Data provider for the test_registration_values tests.
*
* @return array
*/
public function registration_values_provider(): array {
return [
'Null registration' => [
'registration' => null,
'expected' => ['aa', 'bb', 'cc', 'dd'],
],
'Registration with one state id' => [
'registration' => 'reg2',
'expected' => ['cc'],
],
'Registration with two state ids' => [
'registration' => 'reg',
'expected' => ['bb', 'dd'],
],
'Registration with no state ids' => [
'registration' => 'invented',
'expected' => [],
],
];
}
/**
* Generate the state for the testing scenarios.
*
* Generate a variaty of states from several components, registrations and state ids.
* Some of the states are registered as they are done in 27-04-2022 07:00:00 while others
* are updated in 28-04-2022 07:00:00.
*
* @return state[]
*/
private function generate_states(): array {
global $DB;
$testdate = \DateTime::createFromFormat('d-m-Y H:i:s', '28-04-2022 07:00:00');
// Unix timestamp: 1651100400.
$currenttime = $testdate->getTimestamp();
$result = [];
// Add a few xAPI state records to database.
$states = [
['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'bb'],
['activity' => item_activity::create_from_id('1'), 'registration' => 'reg2', 'stateid' => 'cc'],
['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'dd'],
['activity' => item_activity::create_from_id('2'), 'stateid' => 'ee'],
['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'stateid' => 'gg'],
['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'registration' => 'reg', 'stateid' => 'ff'],
];
foreach ($states as $state) {
$result[$state['stateid']] = test_helper::create_state($state, true);
}
$timepast = $currenttime - DAYSECS;
$DB->set_field('xapi_states', 'timecreated', $timepast);
$DB->set_field('xapi_states', 'timemodified', $timepast);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'aa']);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'bb']);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'ee']);
return $result;
}
}
+225
View File
@@ -0,0 +1,225 @@
<?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 core_xapi\external;
use core_xapi\xapi_exception;
use core_xapi\local\statement\item_agent;
use externallib_advanced_testcase;
use core_external\external_api;
use core_xapi\iri;
use core_xapi\local\state;
use core_xapi\test_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI post state webservice.
*
* @package core_xapi
* @covers \core_xapi\external\post_state
* @since Moodle 4.2
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_state_test extends externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/xapi/tests/helper.php');
}
/**
* Testing different component names on valid states.
*
* @dataProvider components_provider
* @param string $component component name
* @param string|null $expected expected results
*/
public function test_component_names(string $component, ?string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$data = test_helper::create_state();
$this->post_state_data($component, $data, $expected);
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'component' => 'inexistent_component',
'expected' => null,
],
'Compatible component' => [
'component' => 'fake_component',
'expected' => 'true',
],
'Incompatible component' => [
'component' => 'core_xapi',
'expected' => null,
],
];
}
/**
* Testing invalid agent.
*
*/
public function test_invalid_agent(): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Invalid agent (use different user, instead of the current one).
$info = [
'agent' => item_agent::create_from_user($other),
];
$data = test_helper::create_state($info);
$this->post_state_data('fake_component', $data, null);
}
/**
* Testing valid/invalid state.
*
* @dataProvider states_provider
* @param string $stateid The xAPI state id.
* @param string|null $expected Expected results.
* @return void
*/
public function test_post_state(string $stateid, ?string $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$info = [
'stateid' => $stateid,
];
$data = test_helper::create_state($info);
$this->post_state_data('fake_component', $data, $expected);
}
/**
* Data provider for the test_post_state tests.
*
* @return array
*/
public function states_provider(): array {
return [
'Empty stateid' => [
'stateid' => '',
'expected' => 'true',
],
'Valid stateid (any value but paella)' => [
'stateid' => 'sangria',
'expected' => 'true',
],
'Invalid stateid' => [
'stateid' => 'paella',
'expected' => null,
],
];
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return post_state the external class
*/
private function get_external_class(): post_state {
$ws = new class extends post_state {
/**
* Method to override validate_component.
*
* @param string $component The component name in frankenstyle.
*/
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* This function do all checks from a standard post_state request.
*
* The reason for this function is because states crafting (special in error
* scenarios) is complicated to do via data providers because every test need a specific
* testing conditions. For this reason alls tests creates a scenario and then uses this
* function to check the results.
*
* @param string $component component name
* @param state $data data to encode and send to post_state
* @param string $expected expected results (if null an exception is expected)
*/
private function post_state_data(string $component, state $data, ?string $expected): void {
global $DB;
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// When no result is expected, an exception will be incurred.
if (is_null($expected)) {
$this->expectException(xapi_exception::class);
}
$external = $this->get_external_class();
$result = $external::execute(
$component,
iri::generate($data->get_activity_id(), 'activity'),
json_encode($data->get_agent()),
$data->get_state_id(),
json_encode($data->jsonSerialize()),
$data->get_registration()
);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
// Check the state has been saved with the expected values.
$this->assertTrue($result);
$records = $DB->get_records('xapi_states');
$this->assertCount($currentstates + 1, $records);
$record = reset($records);
$this->assertEquals($component, $record->component);
$this->assertEquals($data->get_activity_id(), $record->itemid);
$this->assertEquals($data->get_user()->id, $record->userid);
$this->assertEquals(json_encode($data->jsonSerialize()), $record->statedata);
$this->assertEquals($data->get_registration(), $record->registration);
}
}
+490
View File
@@ -0,0 +1,490 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\external;
use core_xapi\xapi_exception;
use core_xapi\test_helper;
use core_xapi\external\post_statement;
use core_xapi\local\statement;
use core_xapi\local\statement\item_agent;
use core_xapi\local\statement\item_group;
use core_xapi\local\statement\item_verb;
use core_xapi\local\statement\item_activity;
use externallib_advanced_testcase;
use stdClass;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Unit tests for xAPI statement processing webservice.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_statement_test extends externallib_advanced_testcase {
/** @var test_helper for generating valid xapi statements. */
private $testhelper;
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Setup test.
*/
public function setUp(): void {
global $CFG;
// We disable group actors on the test xapi_handler.
$CFG->xapitestforcegroupactors = false;
}
/**
* Return a xAPI external webservice class to operate.
*
* The test needs to fake a component in order to test without
* using a real one. This way if in the future any component
* implement it's xAPI handler this test will continue working.
*
* @return post_statement the external class
*/
private function get_extenal_class(): post_statement {
$ws = new class extends post_statement {
protected static function validate_component(string $component): void {
if ($component != 'fake_component') {
parent::validate_component($component);
}
}
};
return $ws;
}
/**
* This function do all checks from a standard post_statements request.
*
* The reason for this function is because statements crafting (special in error
* scenarios) is complicated to do via data providers because every test need a specific
* testing conditions. For this reason alls tests creates a scenario and then uses this
* function to check the results.
*
* @param string $component component name
* @param mixed $data data to encode and send to post_statement
* @param array $expected expected results (i empty an exception is expected)
*/
private function post_statements_data(string $component, $data, array $expected) {
global $USER;
$testhelper = new test_helper();
$testhelper->init_log();
// If no result is expected we will just incur in exception.
if (empty($expected)) {
$this->expectException(xapi_exception::class);
} else {
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
}
$json = json_encode($data);
$external = $this->get_extenal_class();
$result = $external::execute($component, $json);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
// Check results.
$this->assertCount(count($expected), $result);
foreach ($expected as $key => $expect) {
$this->assertEquals($expect, $result[$key]);
}
// Check log entries.
$log = $testhelper->get_last_log_entry();
$this->assertNotEmpty($log);
// Validate statement information on log.
$value = $log->get_name();
$this->assertEquals($value, 'xAPI test statement');
$value = $log->get_description();
// Due to logstore limitation, event must use a real component (core_xapi).
$this->assertEquals($value, "User '{$USER->id}' send a statement to component 'core_xapi'");
}
/**
* Return a valid statement object with the params passed.
*
* All tests are based on craft different types os statements. This function
* is made to provent redundant code on the test.
*
* @param array $items array of overriden statement items (default [])
* @return statement the resulting statement
*/
private function get_valid_statement(array $items = []): statement {
global $USER;
$actor = $items['actor'] ?? item_agent::create_from_user($USER);
$verb = $items['verb'] ?? item_verb::create_from_id('cook');
$object = $items['object'] ?? item_activity::create_from_id('paella');
$statement = new statement();
$statement->set_actor($actor);
$statement->set_verb($verb);
$statement->set_object($object);
return $statement;
}
/**
* Testing different component names on valid statements.
*
* @dataProvider components_provider
* @param string $component component name
* @param array $expected expected results
*/
public function test_component_names(string $component, array $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$data = $this->get_valid_statement();
$this->post_statements_data ($component, $data, $expected);
}
/**
* Data provider for the test_component_names tests.
*
* @return array
*/
public function components_provider(): array {
return [
'Inexistent component' => [
'inexistent_component', []
],
'Compatible component' => [
'fake_component', [true]
],
'Incompatible component' => [
'core_xapi', []
],
];
}
/**
* Testing raw JSON encoding.
*
* This test is used for wrong json format and empty structures.
*
* @dataProvider invalid_json_provider
* @param string $json json string to send
*/
public function test_invalid_json(string $json): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Perform test.
$testhelper = new test_helper();
$testhelper->init_log();
// If no result is expected we will just incur in exception.
$this->expectException(xapi_exception::class);
$external = $this->get_extenal_class();
$result = $external::execute('fake_component', $json);
$result = external_api::clean_returnvalue($external::execute_returns(), $result);
}
/**
* Data provider for the test_components tests.
*
* @return array
*/
public function invalid_json_provider(): array {
return [
'Wrong json' => [
'This is not { a json object /'
],
'Empty string json' => [
''
],
'Empty array json' => [
'[]'
],
'Invalid single statement json' => [
'{"actor":{"objectType":"Agent","mbox":"noemail@moodle.org"},"verb":{"id":"InvalidVerb"}'
.',"object":{"objectType":"Activity","id":"somethingwrong"}}'
],
'Invalid multiple statement json' => [
'[{"actor":{"objectType":"Agent","mbox":"noemail@moodle.org"},"verb":{"id":"InvalidVerb"}'
.',"object":{"objectType":"Activity","id":"somethingwrong"}}]'
],
];
}
/**
* Testing agent (user) statements.
*
* This function test several scenarios using different combinations
* of statement rejection motives. Some motives produces a full batch
* rejection (exception) and other can leed to indivual rejection on
* each statement. For example,try to post a statement without $USER
* in it produces a full batch rejection, while using an invalid
* verb on one statement just reject that specific statement
* That is the expected behaviour.
*
* @dataProvider statement_provider
* @param bool $multiple if send multiple statements (adds one valid statement)
* @param bool $validactor if the actor used is valid
* @param bool $validverb if the verb used is valid
* @param array $expected expected results
*/
public function test_statements_agent(bool $multiple, bool $validactor, bool $validverb, array $expected): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
$info = [];
// Setup actor.
if ($validactor) {
$info['actor'] = item_agent::create_from_user($USER);
} else {
$info['actor'] = item_agent::create_from_user($other);
}
// Setup verb.
if (!$validverb) {
$info['verb'] = item_verb::create_from_id('invalid');
}
$data = $this->get_valid_statement($info);
if ($multiple) {
$data = [
$this->get_valid_statement(),
$data,
];
}
// Perform test.
$this->post_statements_data ('fake_component', $data, $expected);
}
/**
* Testing group statements.
*
* This function test several scenarios using different combinations
* of statement rejection motives. Some motives produces a full batch
* rejection (exception) and other can leed to indivual rejection on
* each statement. For example,try to post a statement without $USER
* in it produces a full batch rejection, while using an invalid
* verb on one statement just reject that specific statement
* That is the expected behaviour.
*
* @dataProvider statement_provider
* @param bool $multiple if send multiple statements (adds one valid statement)
* @param bool $validactor if the actor used is valid
* @param bool $validverb if the verb used is valid
* @param array $expected expected results
*/
public function test_statements_group(bool $multiple, bool $validactor, bool $validverb, array $expected): void {
global $USER, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
$info = [];
// Enable group mode in the handle.
$CFG->xapitestforcegroupactors = true;
// Create one course and 1 group.
$course = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($USER->id, $course->id);
$this->getDataGenerator()->enrol_user($other->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $other->id));
if ($validactor) {
// Add $USER into a group to make group valid for processing.
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $USER->id));
}
$info['actor'] = item_group::create_from_group($group);
// Setup verb.
if (!$validverb) {
$info['verb'] = item_verb::create_from_id('invalid');
}
$data = $this->get_valid_statement($info);
if ($multiple) {
$data = [
$this->get_valid_statement(),
$data,
];
}
// Perform test.
$this->post_statements_data ('fake_component', $data, $expected);
}
/**
* Data provider for the test_components tests.
*
* @return array
*/
public function statement_provider(): array {
return [
// Single statement with group statements enabled.
'Single, Valid actor, valid verb' => [
false, true, true, [true]
],
'Single, Invalid actor, valid verb' => [
false, false, true, []
],
'Single, Valid actor, invalid verb' => [
false, true, false, []
],
'Single, Inalid actor, invalid verb' => [
false, false, false, []
],
// Multi statement with group statements enabled.
'Multiple, Valid actor, valid verb' => [
true, true, true, [true, true]
],
'Multiple, Invalid actor, valid verb' => [
true, false, true, []
],
'Multiple, Valid actor, invalid verb' => [
true, true, false, [true, false]
],
'Multiple, Inalid actor, invalid verb' => [
true, false, false, []
],
];
}
/**
* Test posting group statements to a handler without group actor support.
*
* Try to use group statement in components that not support this feature
* causes a full statements batch rejection.
*
* @dataProvider group_statement_provider
* @param bool $usegroup1 if the 1st statement must be groupal
* @param bool $usegroup2 if the 2nd statement must be groupal
* @param array $expected expected results
*/
public function test_group_disabled(bool $usegroup1, bool $usegroup2, array $expected): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Create one course and 1 group.
$course = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($USER->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $USER->id));
$info = ['actor' => item_group::create_from_group($group)];
$groupstatement = $this->get_valid_statement($info);
$agentstatement = $this->get_valid_statement();
$data = [];
$data[] = ($usegroup1) ? $groupstatement : $agentstatement;
$data[] = ($usegroup2) ? $groupstatement : $agentstatement;
// Perform test.
$this->post_statements_data ('fake_component', $data, $expected);
}
/**
* Data provider for the test_components tests.
*
* @return array
*/
public function group_statement_provider(): array {
return [
// Single statement with group statements enabled.
'Group statement + group statement without group support' => [
true, true, []
],
'Group statement + agent statement without group support' => [
true, false, []
],
'Agent statement + group statement without group support' => [
true, false, []
],
'Agent statement + agent statement without group support' => [
false, false, [true, true]
],
];
}
/**
* Test posting a statements batch not accepted by handler.
*
* If all statements from a batch are rejectes by the plugin the full
* batch is considered rejected and an exception is returned.
*/
public function test_full_batch_rejected(): void {
$this->resetAfterTest();
$this->setAdminUser();
$info = ['verb' => item_verb::create_from_id('invalid')];
$statement = $this->get_valid_statement($info);
$data = [$statement, $statement];
// Perform test.
$this->post_statements_data ('fake_component', $data, []);
}
}
+136
View File
@@ -0,0 +1,136 @@
<?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/>.
/**
* The core_xapi test class for xAPI statements.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace fake_component\xapi;
use core_xapi\local\statement;
use core_xapi\handler as handler_base;
use core_xapi\event\xapi_test_statement_post;
use context_system;
use core\event\base;
use core_xapi\local\state;
defined('MOODLE_INTERNAL') || die();
/**
* Class xapi_handler testing dummie class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
*/
class handler extends handler_base {
/**
* Convert statements to event.
*
* Convert a statement object into a Moodle xAPI Event. If a statement is accepted
* by validate_statement the component must provide a event to handle that statement,
* otherwise the statement will be rejected.
*
* @param statement $statement the statement object
* @return \core\event\base|null a Moodle event to trigger
*/
public function statement_to_event(statement $statement): ?base {
global $USER;
// Validate verb.
$validvalues = [
'cook',
'http://adlnet.gov/expapi/verbs/answered'
];
$verbid = $statement->get_verb_id();
if (!in_array($verbid, $validvalues)) {
return null;
}
// Validate object.
$validvalues = [
'paella',
'http://adlnet.gov/expapi/activities/example'
];
$activityid = $statement->get_activity_id();
if (!in_array($activityid, $validvalues)) {
return null;
}
if ($this->supports_group_actors()) {
$users = $statement->get_all_users();
// In most cases we can use $USER->id as the event userid but because
// this is just a test class it checks first for $USER and, if not
// present just pick the first one.
$user = $users[$USER->id] ?? array_shift($users);
} else {
$user = $statement->get_user();
}
// Convert into a Moodle event.
$minstatement = $statement->minify();
$params = array(
'other' => $minstatement,
'context' => context_system::instance(),
'userid' => $user->id,
);
return xapi_test_statement_post::create($params);
}
/**
* Return true if group actor is enabled.
*
* NOTE: the use of a global is only for testing. We need to change
* the behaviour from the PHPUnitTest to test all possible scenarios.
*
* Note: this method must be overridden by the plugins which want to
* use groups in statements.
*
* @return bool
*/
public function supports_group_actors(): bool {
global $CFG;
if (isset($CFG->xapitestforcegroupactors)) {
return $CFG->xapitestforcegroupactors;
}
return parent::supports_group_actors();
}
/**
* Validate a xAPI state.
*
* Check if the state is valid for this handler.
*
* This method is used also for the state get requests so the validation
* cannot rely on having state data.
*
* @param state $state
* @return bool if the state is valid or not
*/
protected function validate_state(state $state): bool {
// For testing purposes, the state will be considered NOT valid when stateid is set to 'paella'.
if ($state->get_state_id() === 'paella') {
return false;
}
return true;
}
}
+102
View File
@@ -0,0 +1,102 @@
<?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/>.
/**
* Mock events for xAPI testing.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\event;
use context_system;
use core_xapi\local\statement;
defined('MOODLE_INTERNAL') || die();
/**
* xAPI statement webservice testing event.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class xapi_test_statement_post extends \core\event\base {
/**
* Initialise the event data.
*/
protected function init() {
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return "xAPI test statement";
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "User '$this->userid' send a statement to component '$this->component'";
}
/**
* Compare if a given statement is similar to the one on the record.
*
* The information stored in the logstore is not exactly a xAPI standard.
* Similar checks for actor, verb, object (+ definition) and result for now.
*
* @param statement $statement An xAPI compatible statement.
* @return bool True if the $statement represents this event.
*/
public function compare_statement(statement $statement): bool {
// Check minified version.
$calculatedfields = ['actor', 'id', 'timestamp', 'stored', 'version'];
foreach ($calculatedfields as $field) {
if (isset($this->data['other'][$field])) {
return false;
}
}
// Check verb structure.
$data = $statement->get_verb()->get_data();
if ($this->data['other']['verb']['id'] != $data->id) {
return false;
}
// Check user.
$users = $statement->get_all_users();
if (empty($users) || !isset($users[$this->data['userid']])) {
return false;
}
// Check object.
$data = $statement->get_object()->get_data();
if ($this->data['other']['object']['id'] != $data->id) {
return false;
}
return true;
}
}
+350
View File
@@ -0,0 +1,350 @@
<?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 core_xapi;
use core_xapi\xapi_exception;
use core_xapi\local\statement;
use core_xapi\local\statement\item_agent;
use core_xapi\local\statement\item_verb;
use core_xapi\local\statement\item_activity;
use advanced_testcase;
use core_xapi\local\state;
use stdClass;
/**
* Contains test cases for testing xAPI handler base methods.
*
* @package core_xapi
* @since Moodle 3.9
* @covers \core_xapi\handler
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class handler_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Test handler creation.
*/
public function test_handler_create(): void {
// Get an existent handler.
$handler = handler::create('fake_component');
$this->assertEquals(get_class($handler), 'fake_component\\xapi\\handler');
// Get a non existent handler.
$this->expectException(xapi_exception::class);
$value = handler::create('potato_omelette');
}
/**
* Test xAPI support.
*/
public function test_supports_xapi(): void {
// Get an existent handler.
$result = handler::supports_xapi('fake_component');
$this->assertTrue($result);
// Get a non existent handler.
$result = handler::supports_xapi('potato_omelette');
$this->assertFalse($result);
}
/**
* Test support group.
*/
public function test_support_group_actor(): void {
global $CFG;
// Get an existent handler.
$this->resetAfterTest();
$handler = handler::create('fake_component');
$this->assertEquals(get_class($handler), 'fake_component\\xapi\\handler');
$CFG->xapitestforcegroupactors = false;
$this->assertEquals(false, $handler->supports_group_actors());
}
/**
* Test for process_statements method.
*/
public function test_process_statements(): void {
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
$user = $this->getDataGenerator()->create_user();
$testhelper = new test_helper();
$testhelper->init_log();
// Generate a 2 statements array (one accepted one not).
$statements = [];
$statement = new statement();
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_activity::create_from_id('paella'));
$statements[] = $statement;
$statement2 = new statement();
$statement2->set_actor(item_agent::create_from_user($user));
$statement2->set_verb(item_verb::create_from_id('invalid'));
$statement2->set_object(item_activity::create_from_id('paella'));
$statements[] = $statement2;
$handler = handler::create('fake_component');
$result = $handler->process_statements($statements);
// Check results.
$this->assertCount(2, $result);
$this->assertEquals(true, $result[0]);
$this->assertEquals(false, $result[1]);
// Check log entries.
/** @var \core_xapi\event\xapi_test_statement_post $log */
$log = $testhelper->get_last_log_entry();
$this->assertNotEmpty($log);
// Validate statement information on log.
$value = $log->get_name();
$this->assertEquals($value, 'xAPI test statement');
$value = $log->get_description();
// Due to logstore limitation, event must use a real component (core_xapi).
$this->assertEquals($value, 'User \''.$user->id.'\' send a statement to component \'core_xapi\'');
$this->assertTrue($log->compare_statement($statement));
}
/**
* Testing save_state method.
*/
public function test_save_state(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = 'fake_component';
$handler = handler::create($component);
// Check the state has been added.
$state = test_helper::create_state();
$this->assertEquals(0, $DB->count_records('xapi_states'));
$result = $handler->save_state($state);
$this->assertTrue($result);
$records = $DB->get_records('xapi_states');
$this->assertCount(1, $records);
$record = reset($records);
$this->check_state($component, $state, $record);
// Check the state has been updated.
$statedata = '{"progress":0,"answers":[[["BB"],[""]],[{"answers":[]}]],"answered":[true,false]}';
$state->set_state_data(json_decode($statedata));
$result = $handler->save_state($state);
$this->assertTrue($result);
$records = $DB->get_records('xapi_states');
$this->assertCount(1, $records);
$record = reset($records);
$this->check_state($component, $state, $record);
// Check an exception is thrown when the state is not valid.
$this->expectException(xapi_exception::class);
$state = test_helper::create_state(['stateid' => 'paella']);
$result = $handler->save_state($state);
}
/**
* Testing load_state method.
*/
public function test_load_state(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = 'fake_component';
$handler = handler::create($component);
// Check the state is not found (when there are no states).
$state = test_helper::create_state();
$state->set_state_data(null);
$this->assertEquals(0, $DB->count_records('xapi_states'));
$result = $handler->load_state($state);
$this->assertEquals(0, $DB->count_records('xapi_states'));
$this->assertNull($result);
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Check the state is found when it exists.
$result = $handler->load_state($state);
$records = $DB->get_records('xapi_states');
$this->assertCount(1, $records);
$record = reset($records);
$this->check_state($component, $state, $record);
$this->assertEquals($state->jsonSerialize(), $result->jsonSerialize());
// Check the state is not found when it doesn't exist.
$state = test_helper::create_state(['activity' => item_activity::create_from_id('1')]);
$state->set_state_data(null);
$result = $handler->load_state($state);
$records = $DB->get_records('xapi_states');
$this->assertCount(1, $DB->get_records('xapi_states'));
$this->assertNull($result);
// Check an exception is thrown when the state is not valid.
$this->expectException(xapi_exception::class);
$state = test_helper::create_state(['stateid' => 'paella']);
$result = $handler->load_state($state);
}
/**
* Testing delete_state method.
*/
public function test_delete_state(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = 'fake_component';
$handler = handler::create($component);
// Check the state is not deleted (when there are no states).
$state = test_helper::create_state();
$this->assertEquals(0, $DB->count_records('xapi_states'));
$result = $handler->delete_state($state);
$this->assertTrue($result);
$this->assertEquals(0, $DB->count_records('xapi_states'));
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Check the state is not deleted if the given state doesn't meet its values.
$state2 = test_helper::create_state(['activity' => item_activity::create_from_id('1')]);
$result = $handler->delete_state($state2);
$this->assertTrue($result);
$this->assertCount(1, $DB->get_records('xapi_states'));
// Check the state is deleted if it exists.
$result = $handler->delete_state($state);
$this->assertTrue($result);
$this->assertCount(0, $DB->get_records('xapi_states'));
// Check an exception is thrown when the state is not valid.
$this->expectException(xapi_exception::class);
$state = test_helper::create_state(['stateid' => 'paella']);
$result = $handler->delete_state($state);
}
/**
* Testing reset_states method.
*/
public function test_reset_states(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = 'fake_component';
$handler = handler::create($component);
// Check the state is not reset (when there are no states).
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
$handler->reset_states();
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Check the state is not reset if the given state doesn't meet its values.
$handler->reset_states('1');
$this->assertCount(1, $DB->get_records('xapi_states'));
$this->assertCount(0, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
// Check the state is reset if it exists.
$handler->reset_states();
$this->assertCount(1, $DB->get_records('xapi_states'));
$this->assertCount(1, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
// Check the state is reset too when using some of the given parameters.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
$handler->reset_states('1');
$this->assertCount(2, $DB->get_records('xapi_states'));
$this->assertCount(2, $DB->get_records_select('xapi_states', 'statedata IS NULL'));
}
/**
* Testing wipe_states method.
*/
public function test_wipe_states(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$component = 'fake_component';
$handler = handler::create($component);
// Check the state is not wiped (when there are no states).
$this->assertCount(0, $DB->get_records('xapi_states'));
$handler->wipe_states();
$this->assertCount(0, $DB->get_records('xapi_states'));
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Check the state is not wiped if the given state doesn't meet its values.
$handler->wipe_states('1');
$this->assertCount(1, $DB->get_records('xapi_states'));
// Check the state is wiped if it exists.
$handler->wipe_states();
$this->assertCount(0, $DB->get_records('xapi_states'));
// Check the state is wiped too when using some of the given parameters.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
$this->assertCount(1, $DB->get_records('xapi_states'));
$handler->wipe_states('1');
$this->assertCount(0, $DB->get_records('xapi_states'));
}
/**
* Check if the given state and record are equals.
*
* @param string $component The component name in frankenstyle.
* @param state $state The state to check.
* @param stdClass $record The record to be compared with the state.
*/
private function check_state(string $component, state $state, stdClass $record): void {
$this->assertEquals($component, $record->component);
$this->assertEquals($state->get_activity_id(), $record->itemid);
$this->assertEquals($state->get_user()->id, $record->userid);
$this->assertEquals(json_encode($state->jsonSerialize()), $record->statedata);
$this->assertEquals($state->get_registration(), $record->registration);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?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/>.
/**
* Unit tests helper for xAPI library.
*
* This file contains unit test helpers related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\local\statement\item_agent;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/handler.php');
require_once(__DIR__ . '/fixtures/xapi_test_statement_post.php');
/**
* Contains helper functions for xAPI PHPUnit Tests.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_helper {
/** @var \core\log\reader contains a valid logstore reader. */
private $store;
/**
* Constructor for a xAPI test helper.
*
*/
public function init_log() {
// Enable logs.
set_config('jsonformat', 1, 'logstore_standard');
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
set_config('logguests', 1, 'logstore_standard');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$this->store = $stores['logstore_standard'];
}
/**
* Return the last log entry from standardlog.
*
* @return \core\event\base|null The last log event or null if none found.
*/
public function get_last_log_entry(): ?\core\event\base {
$select = "component = :component";
$params = ['component' => 'core_xapi'];
$records = $this->store->get_events_select($select, $params, 'timecreated DESC', 0, 1);
if (empty($records)) {
return null;
}
return array_pop($records);
}
/**
* Return a valid state object with the params passed.
*
* All tests are based on craft different types of states. This function
* is made to prevent redundant code on the test.
*
* @param array $info array of overriden state data (default []).
* @param bool $createindatabase Whether the state object should be created in database too or not.
* @return state the resulting state
*/
public static function create_state(array $info = [], bool $createindatabase = false): state {
global $USER;
$component = $info['component'] ?? 'fake_component';
$agent = $info['agent'] ?? item_agent::create_from_user($USER);
$activity = $info['activity'] ?? item_activity::create_from_id('12345');
$stateid = $info['stateid'] ?? 'state';
$data = $info['data'] ?? json_decode('{"progress":0,"answers":[[["AA"],[""]],[{"answers":[]}]],"answered":[true,false]}');
$registration = $info['registration'] ?? null;
$state = new state($agent, $activity, $stateid, (object)$data, $registration);
if ($createindatabase) {
try {
$handler = handler::create($component);
$statestore = $handler->get_state_store();
} catch (\Exception $exception) {
// If the component is not available, use the standard one to force it's creation.
$statestore = new state_store($component);
}
$statestore->put($state);
}
return $state;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi;
use advanced_testcase;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing xAPI iri class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class iri_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Test IRI generation.
*
* @dataProvider iri_samples_provider
* @param string $value Value to generate IRI
* @param string $expected Expected result
* @param string $type = null If some special type is provided
*/
public function test_generate(string $value, string $expected, string $type = null): void {
$iri = iri::generate($value, $type);
$this->assertEquals($iri, $expected);
}
/**
* Test IRI extraction.
*
* @dataProvider iri_samples_provider
* @param string $expected Expected result
* @param string $value Value to generate IRI
* @param string $type = null If some special type is provided
*/
public function test_extract(string $expected, string $value, string $type = null): void {
$extract = iri::extract($value, $type);
$this->assertEquals($extract, $expected);
}
/**
* Data provider for the test_generate and test_extract tests.
*
* @return array
*/
public function iri_samples_provider(): array {
global $CFG;
return [
'Fake IRI without type' => [
'paella',
"{$CFG->wwwroot}/xapi/element/paella",
null,
],
'Real IRI without type' => [
'http://adlnet.gov/expapi/activities/example',
'http://adlnet.gov/expapi/activities/example',
null,
],
'Fake IRI with type' => [
'paella',
"{$CFG->wwwroot}/xapi/dish/paella",
'dish',
],
'Real IRI with type' => [
'http://adlnet.gov/expapi/activities/example',
'http://adlnet.gov/expapi/activities/example',
'dish',
],
];
}
/**
* Test IRI generation.
*
* @dataProvider iri_check_provider
* @param string $value Value to generate IRI
* @param bool $expected Expected result
*/
public function test_check(string $value, bool $expected): void {
$check = iri::check($value);
$this->assertEquals($check, $expected);
}
/**
* Data provider for the test_check.
*
* @return array
*/
public function iri_check_provider(): array {
return [
'Real IRI http' => [
'http://adlnet.gov/expapi/activities/example',
true,
],
'Real IRI https' => [
'https://adlnet.gov/expapi/activities/example',
true,
],
'Invalid IRI' => [
'paella',
false,
],
];
}
}
@@ -0,0 +1,193 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement activity class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_activity_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_creation(): void {
// Activity without definition.
$data = (object) [
'objectType' => 'Activity',
'id' => iri::generate('paella', 'activity'),
];
$item = item_activity::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals($item->get_id(), 'paella');
$this->assertNull($item->get_definition());
// Add optional objectType.
$data->objectType = 'Activity';
$item = item_activity::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
// Add definition.
$data->definition = (object) [
'interactionType' => 'choice',
];
$item = item_activity::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertNotNull($item->get_definition());
}
/**
* Test item creation from string.
*
* @dataProvider create_from_id_provider
* @param string $id Object string ID (IRI or not)
* @param bool $usedefinition if a valir definition must be attached or not
*/
public function test_create_from_id(string $id, bool $usedefinition): void {
$definition = null;
if ($usedefinition) {
$data = (object) [
'type' => iri::generate('example', 'id'),
'interactionType' => 'choice'
];
$definition = item_definition::create_from_data($data);
}
$item = item_activity::create_from_id($id, $definition);
$this->assertEquals($id, $item->get_id());
$itemdefinition = $item->get_definition();
if ($usedefinition) {
$this->assertEquals('choice', $itemdefinition->get_interactiontype());
} else {
$this->assertNull($itemdefinition);
}
// Check generated data.
$data = $item->get_data();
$this->assertEquals('Activity', $data->objectType);
$this->assertEquals(iri::generate($id, 'activity'), $data->id);
if ($usedefinition) {
$this->assertEquals('choice', $data->definition->interactionType);
}
}
/**
* Data provider for the test_create_from_id tests.
*
* @return array
*/
public function create_from_id_provider(): array {
return [
'Fake IRI with no definition' => [
'paella', false,
],
'Fake IRI with definition' => [
'paella', true,
],
'Real IRI with no definition' => [
'http://adlnet.gov/expapi/activities/example', false,
],
'Real IRI with definition' => [
'http://adlnet.gov/expapi/activities/example', true,
],
];
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param string $type objectType attribute
* @param string $id activity ID
*/
public function test_invalid_data(string $type, string $id): void {
$data = (object) [
'objectType' => $type,
];
if (!empty($id)) {
$data->id = $id;
}
$this->expectException(xapi_exception::class);
$item = item_activity::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'Invalid Avtivity objectType' => [
'Invalid Type!', iri::generate('paella', 'activity'),
],
'Invalid id value' => [
'Activity', 'Invalid_iri_value',
],
'Non-existent id value' => [
'Activity', '',
],
];
}
/**
* Test for missing object type.
*/
public function test_missing_object_type(): void {
$data = (object) ['id' => 42];
$this->expectException(xapi_exception::class);
$item = item_activity::create_from_data($data);
}
/**
* Test for invalid activity objectType.
*/
public function test_inexistent_agent(): void {
global $CFG;
$data = (object) [
'objectType' => 'Invalid',
'id' => -1,
];
$this->expectException(xapi_exception::class);
$item = item_activity::create_from_data($data);
}
}
@@ -0,0 +1,115 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement actor class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_actor_test extends advanced_testcase {
/**
* Test item creation with agent.
*/
public function test_creation_agent(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$item = item_agent::create_from_user($user);
$data = $item->get_data();
$item = item_actor::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_agent', get_class($item));
// Create without specify type.
unset($data->objectType);
$item = item_actor::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_agent', get_class($item));
// Check user.
$itemuser = $item->get_user();
$this->assertEquals($itemuser->id, $user->id);
$itemusers = $item->get_all_users();
$this->assertCount(1, $itemusers);
}
/**
* Test item creation with group.
*/
public function test_creation_group(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
$item = item_group::create_from_group($group);
$data = $item->get_data();
$item = item_actor::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_group', get_class($item));
// Check group.
$itemgroup = $item->get_group();
$this->assertEquals($itemgroup->id, $group->id);
$itemusers = $item->get_all_users();
$this->assertCount(1, $itemusers);
// Code must prevent from using group as a single user.
$this->expectException(xapi_exception::class);
$itemusers = $item->get_user();
}
/**
* Test for invalid structures.
*/
public function test_invalid_data(): void {
$this->expectException(xapi_exception::class);
$data = (object) [
'objectType' => 'Fake',
];
$item = item_actor::create_from_data($data);
}
}
@@ -0,0 +1,253 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement agent class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_agent_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
global $CFG;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
// Ceate using account.
$data = (object) [
'objectType' => 'Agent',
'account' => (object) [
'homePage' => $CFG->wwwroot,
'name' => $user->id,
],
];
$item = item_agent::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$itemuser = $item->get_user();
$this->assertEquals($itemuser->id, $user->id);
$itemusers = $item->get_all_users();
$this->assertCount(1, $itemusers);
// Ceate using mbox.
$data = (object) [
'objectType' => 'Agent',
'mbox' => $user->email,
];
$item = item_agent::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$itemuser = $item->get_user();
$this->assertEquals($itemuser->id, $user->id);
$itemusers = $item->get_all_users();
$this->assertCount(1, $itemusers);
}
/**
* Test item creation from Record.
*/
public function test_create_from_user(): void {
global $CFG;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$item = item_agent::create_from_user($user);
$itemuser = $item->get_user();
$this->assertEquals($itemuser->id, $user->id);
$itemusers = $item->get_all_users();
$this->assertCount(1, $itemusers);
// Check generated data.
$data = $item->get_data();
$this->assertEquals('Agent', $data->objectType);
$this->assertEquals($CFG->wwwroot, $data->account->homePage);
$this->assertEquals($user->id, $data->account->name);
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param string $objecttype object type attribute
* @param bool $validhome if valid homepage is user
* @param bool $validid if valid group id is used
*/
public function test_invalid_data(string $objecttype, bool $validhome, bool $validid): void {
global $CFG;
// Create one course with a group if necessary.
$id = 'Wrong ID';
if ($validid) {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$id = $user->id;
}
$homepage = 'Invalid homepage!';
if ($validhome) {
$homepage = $CFG->wwwroot;
}
$data = (object) [
'objectType' => $objecttype,
'account' => (object) [
'homePage' => $homepage,
'name' => $id,
],
];
$this->expectException(xapi_exception::class);
$item = item_agent::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'Wrong objecttype' => [
'Invalid', true, true
],
'Wrong homepage' => [
'Agent', false, true
],
'Wrong id' => [
'Agent', true, false
],
];
}
/**
* Test non supported account identifier xAPI formats.
*
* @dataProvider unspupported_create_provider
* @param bool $usembox
* @param bool $useaccount
* @param bool $usesha1
* @param bool $useopenid
*/
public function test_unspupported_create(bool $usembox, bool $useaccount, bool $usesha1, bool $useopenid): void {
global $CFG;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
// Ceate using both account and mbox.
$data = (object) [
'objectType' => 'Agent'
];
if ($usembox) {
$data->mbox = $user->email;
}
if ($useaccount) {
$data->account = (object) [
'homePage' => $CFG->wwwroot,
'name' => $user->id,
];
}
if ($usesha1) {
$data->mbox_sha1sum = sha1($user->email);
}
if ($useopenid) {
// Note: this is not a real openid, it's just a value to test.
$data->openid = 'https://www.moodle.openid.com/accounts/o8/id';
}
$this->expectException(xapi_exception::class);
$item = item_agent::create_from_data($data);
}
/**
* Data provider for the unsupported identifiers tests.
*
* @return array
*/
public function unspupported_create_provider(): array {
return [
'Both mbox and account' => [
true, true, false, false
],
'Email SHA1' => [
false, false, false, false
],
'Open ID' => [
false, false, false, false
],
];
}
/**
* Test for missing object type.
*/
public function test_missing_object_type(): void {
$data = (object) ['id' => -1];
$this->expectException(xapi_exception::class);
$item = item_agent::create_from_data($data);
}
/**
* Test for invalid user id.
*/
public function test_inexistent_agent(): void {
global $CFG;
$data = (object) [
'objectType' => 'Agent',
'account' => (object) [
'homePage' => $CFG->wwwroot,
'name' => 0,
],
];
$this->expectException(xapi_exception::class);
$item = item_agent::create_from_data($data);
}
/**
* Test for invalid agent record.
*/
public function test_inexistent_agent_id(): void {
$user = (object) ['name' => 'Me'];
$this->expectException(xapi_exception::class);
$item = item_agent::create_from_user($user);
}
}
@@ -0,0 +1,122 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\iri;
use core_xapi\xapi_exception;
/**
* Contains test cases for testing statement attachment class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_attachment_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
$data = $this->get_generic_data();
$item = item_attachment::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
}
/**
* return a generic data to create a valid item.
*
* @return \stdClass the creation data
*/
private function get_generic_data(): \stdClass {
return (object) [
'usageType' => iri::generate('example', 'attachment'),
'display' => (object) [
'en-US' => 'Example',
],
'description' => (object) [
'en-US' => 'Description example',
],
"contentType" => "image/jpg",
"length" => 1234,
"sha2" => "b94c0f1cffb77475c6f1899111a0181efe1d6177"
];
}
/**
* Test for invalid values.
*
* @dataProvider invalid_values_data
* @param string $attr attribute to modify
* @param mixed $newvalue new value (null means unset)
*/
public function test_invalid_values(string $attr, $newvalue): void {
$data = $this->get_generic_data();
if ($newvalue === null) {
unset($data->$attr);
} else {
$data->$attr = $newvalue;
}
$this->expectException(xapi_exception::class);
$item = item_attachment::create_from_data($data);
}
/**
* Data provider for the test_invalid_values tests.
*
* @return array
*/
public function invalid_values_data(): array {
return [
'No usageType attachment' => [
'usageType', null
],
'Invalid usageType attachment' => [
'usageType', 'Invalid IRI'
],
'No display attachment' => [
'display', null
],
'No contentType attachment' => [
'contentType', null
],
'No length attachment' => [
'length', null
],
'Invalid length attachment' => [
'length', 'Invalid'
],
'No sha2 attachment' => [
'sha2', null
],
];
}
}
@@ -0,0 +1,62 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
/**
* Contains test cases for testing statement context class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_context_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
$data = $this->get_generic_data();
$item = item_context::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
}
/**
* Return a generic data to create a valid item.
*
* @return sdtClass the creation data
*/
private function get_generic_data(): \stdClass {
// For now context has no data validation so a generic data is enough.
return (object) [
'usageType' => '51a6f860-1997-11e3-8ffd-0800200c9a66',
];
}
}
@@ -0,0 +1,104 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement definition class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_definition_test extends advanced_testcase {
/**
* Test item_definition creation.
*
* @dataProvider creation_provider
* @param string $interactiontype
*/
public function test_creation(string $interactiontype): void {
// Activity without interactionType.
$data = (object) [
'type' => iri::generate('example', 'id'),
];
// Add interactionType.
if (!empty($interactiontype)) {
$data->interactionType = $interactiontype;
}
$item = item_definition::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
if (empty($interactiontype)) {
$this->assertNull($item->get_interactiontype());
} else {
$this->assertEquals($interactiontype, $item->get_interactiontype());
}
}
/**
* Data provider for the test_creation tests.
*
* @return array
*/
public function creation_provider(): array {
return [
'No interactionType' => [''],
'Choice' => ['choice'],
'fill-in' => ['fill-in'],
'long-fill-in' => ['long-fill-in'],
'true-false' => ['true-false'],
'matching' => ['matching'],
'performance' => ['performance'],
'sequencing' => ['sequencing'],
'likert' => ['likert'],
'numeric' => ['numeric'],
'other' => ['other'],
'compound' => ['compound'],
];
}
/**
* Test for invalid structures.
*/
public function test_invalid_data(): void {
// Activity without interactionType.
$data = (object) [
'interactionType' => 'Invalid value!',
];
$this->expectException(xapi_exception::class);
$item = item_definition::create_from_data($data);
}
}
@@ -0,0 +1,204 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement group class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_group_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
global $CFG;
$this->resetAfterTest();
// Create one course with a group.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$user2 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user2->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user2->id));
$data = (object) [
'objectType' => 'Group',
'account' => (object) [
'homePage' => $CFG->wwwroot,
'name' => $group->id,
],
];
$item = item_group::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$itemgroup = $item->get_group();
$this->assertEquals($itemgroup->id, $group->id);
$itemusers = $item->get_all_users();
$this->assertCount(2, $itemusers);
// Get user in group item must throw an exception.
$this->expectException(xapi_exception::class);
$itemusers = $item->get_user();
}
/**
* Test item creation from Record.
*/
public function test_create_from_group(): void {
global $CFG;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$item = item_group::create_from_group($group);
$itemgroup = $item->get_group();
$this->assertEquals($itemgroup->id, $group->id);
// Check generated data.
$data = $item->get_data();
$this->assertEquals('Group', $data->objectType);
$this->assertEquals($CFG->wwwroot, $data->account->homePage);
$this->assertEquals($group->id, $data->account->name);
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param string $objecttype object type attribute
* @param bool $validhome if valid homepage is user
* @param bool $validid if valid group id is used
*/
public function test_invalid_data(string $objecttype, bool $validhome, bool $validid): void {
global $CFG;
// Create one course with a group if necessary.
$id = 'Wrong ID';
if ($validid) {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$id = $group->id;
}
$homepage = 'Invalid homepage!';
if ($validhome) {
$homepage = $CFG->wwwroot;
}
$data = (object) [
'objectType' => $objecttype,
'account' => (object) [
'homePage' => $homepage,
'name' => $id,
],
];
$this->expectException(xapi_exception::class);
$item = item_group::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'Wrong objecttype' => [
'Invalid', true, true
],
'Wrong homepage' => [
'Group', false, true
],
'Wrong id' => [
'Group', true, false
],
];
}
/**
* Test for missing object type.
*/
public function test_missing_object_type(): void {
$data = (object) ['id' => -1];
$this->expectException(xapi_exception::class);
$item = item_group::create_from_data($data);
}
/**
* Test for invalid anonymous group.
*/
public function test_invalid_anonymous_group(): void {
$data = (object) [
'objectType' => 'Group'
];
$this->expectException(xapi_exception::class);
$item = item_group::create_from_data($data);
}
/**
* Test for invalid anonymous group.
*/
public function test_inexistent_group(): void {
global $CFG;
$data = (object) [
'objectType' => 'Group',
'account' => (object) [
'homePage' => $CFG->wwwroot,
'name' => 0,
],
];
$this->expectException(xapi_exception::class);
$item = item_group::create_from_data($data);
}
/**
* Test for invalid group record.
*/
public function test_inexistent_group_id(): void {
$group = (object) ['name' => 'My Group'];
$this->expectException(xapi_exception::class);
$item = item_group::create_from_group($group);
}
}
@@ -0,0 +1,130 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement object class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_object_test extends advanced_testcase {
/**
* Test item creation with agent.
*/
public function test_creation_agent(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$item = item_agent::create_from_user($user);
$data = $item->get_data();
$item = item_object::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_agent', get_class($item));
}
/**
* Test item creation with group.
*/
public function test_creation_group(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$item = item_group::create_from_group($group);
$data = $item->get_data();
$item = item_object::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_group', get_class($item));
}
/**
* Test item creation with activity.
*/
public function test_creation_activity(): void {
$item = item_activity::create_from_id('paella');
$data = $item->get_data();
$item = item_object::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals('core_xapi\local\statement\item_activity', get_class($item));
}
/**
* Test unsupported item creation.
*/
public function test_unsupported_activity(): void {
$this->expectException(xapi_exception::class);
$data = (object) [
'objectType' => 'FakeType',
'id' => -1,
];
$item = item_object::create_from_data($data);
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param string $id
*/
public function test_invalid_data(string $id): void {
$this->expectException(xapi_exception::class);
$data = (object) [
'id' => $id,
];
$item = item_object::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'Empty or null id' => [
'',
],
'Invalid IRI value' => [
'invalid_iri_value',
],
];
}
}
@@ -0,0 +1,138 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
/**
* Contains test cases for testing statement result class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_result_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_creation(): void {
$data = $this->get_generic_data();
$item = item_result::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertNull($item->get_duration());
$score = $item->get_score();
$this->assertEquals(json_encode($score), json_encode($data->score));
}
/**
* Return a generic data to create a valid item.
*
* @return \stdClass the creation data
*/
private function get_generic_data(): \stdClass {
return (object) [
'score' => (object)[
'min' => 0,
'max' => 100,
'raw' => 50,
'scaled' => 0.5,
],
'completion' => true,
'success' => true,
];
}
/**
* Test for duration values.
*
* @dataProvider duration_values_data
* @param string|null $duration specified duration
* @param int|null $seconds calculated seconds
* @param bool $exception if exception is expected
*/
public function test_duration_values(?string $duration, ?int $seconds, bool $exception): void {
if ($exception) {
$this->expectException(xapi_exception::class);
}
$data = $this->get_generic_data();
if ($duration !== null) {
$data->duration = $duration;
}
$item = item_result::create_from_data($data);
$this->assertEquals($seconds, $item->get_duration());
}
/**
* Data provider for the test_duration_values tests.
*
* @return array
*/
public function duration_values_data(): array {
return [
'No duration' => [
null, null, false
],
'Empty duration' => [
'', null, false
],
'1 minute duration' => [
'PT1M', 60, false
],
'1 hour duration' => [
'PT1H', 3600, false
],
'1 second duration' => [
'PT1S', 1, false
],
'1.11 second duration (dot variant)' => [
'PT1.11S', 1, false
],
'1,11 second duration (comma variant)' => [
'PT1.11S', 1, false
],
'90 minutes 5 seconds duration' => [
'PT1H30M5S', 5405, false
],
'90 minutes 05 seconds duration' => [
'PT1H30M05S', 5405, false
],
'Half year duration' => [
'P0.5Y', null, true
],
'Incorrect format' => [
'INVALID', null, true
],
];
}
}
@@ -0,0 +1,56 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
/**
* Contains test cases for testing statement score class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_score_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
$data = (object) [
'scaled' => 0.5,
'raw' => 5,
'min' => 0,
'max' => 10,
];
$item = item_score::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
}
}
@@ -0,0 +1,58 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement base class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_create(): void {
// This is a generic item so check that it can create and item and json encode later.
$data = (object) [
'this' => 'is',
'just' => 1,
'example' => ['of', 'structure'],
];
$item = item::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
}
}
@@ -0,0 +1,118 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local\statement;
use advanced_testcase;
use core_xapi\xapi_exception;
use core_xapi\iri;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement verb class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class item_verb_test extends advanced_testcase {
/**
* Test item creation.
*/
public function test_creation(): void {
$data = (object) [
'id' => iri::generate('cook', 'verb'),
];
$item = item_verb::create_from_data($data);
$this->assertEquals(json_encode($item), json_encode($data));
$this->assertEquals($item->get_id(), 'cook');
}
/**
* Test item creation from string.
*
* @dataProvider create_from_id_provider
* @param string $id Object string ID (IRI or not)
*/
public function test_create_from_id(string $id): void {
$item = item_verb::create_from_id($id);
$this->assertEquals($id, $item->get_id());
// Check generated data.
$data = $item->get_data();
$this->assertEquals(iri::generate($id, 'verb'), $data->id);
}
/**
* Data provider for the test_create_from_id tests.
*
* @return array
*/
public function create_from_id_provider(): array {
return [
'Fake IRI' => [
'cook',
],
'Real IRI' => [
'http://adlnet.gov/expapi/verb/example',
],
];
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param string $id
*/
public function test_invalid_data(string $id): void {
$this->expectException(xapi_exception::class);
$data = (object) [
'id' => $id,
];
$item = item_verb::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'Empty or null id' => [
'',
],
'Invalid IRI value' => [
'invalid_iri_value',
],
];
}
}
+588
View File
@@ -0,0 +1,588 @@
<?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/>.
/**
* This file contains unit test related to xAPI library.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local;
use core_xapi\local\statement\item;
use core_xapi\local\statement\item_actor;
use core_xapi\local\statement\item_object;
use core_xapi\local\statement\item_activity;
use core_xapi\local\statement\item_verb;
use core_xapi\local\statement\item_agent;
use core_xapi\local\statement\item_group;
use core_xapi\local\statement\item_result;
use core_xapi\local\statement\item_attachment;
use core_xapi\local\statement\item_context;
use core_xapi\iri;
use core_xapi\xapi_exception;
use advanced_testcase;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* Contains test cases for testing statement class.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statement_test extends advanced_testcase {
/**
* Returns a valid item for a specific attribute.
*
* @param string $itemname statement item name
* @return item the resulting item
*/
private function get_valid_item(string $itemname): item {
global $USER, $CFG;
switch ($itemname) {
case 'attachments':
case 'attachment':
$data = (object) [
'usageType' => iri::generate('example', 'attachment'),
'display' => (object) [
'en-US' => 'Example',
],
'description' => (object) [
'en-US' => 'Description example',
],
"contentType" => "image/jpg",
"length" => 1234,
"sha2" => "b94c0f1cffb77475c6f1899111a0181efe1d6177"
];
return item_attachment::create_from_data($data);
case 'authority':
$data = (object) [
'objectType' => 'Agent',
'account' => (object) [
'homePage' => $CFG->wwwroot,
'name' => $USER->id,
],
];
return item_agent::create_from_data($data);
}
// For now, the rest of the optional properties have no validation
// so we create a standard stdClass for all of them.
$data = (object)[
'some' => 'data',
];
$classname = 'core_xapi\local\statement\item_'.$itemname;
if (class_exists($classname)) {
$item = $classname::create_from_data($data);
} else {
$item = item::create_from_data($data);
}
return $item;
}
/**
* Test statement creation.
*
* @dataProvider create_provider
* @param bool $useagent if use agent as actor (or group if false)
* @param array $extras extra item elements
* @param array $extravalues extra string values
*/
public function test_create(bool $useagent, array $extras, array $extravalues): void {
$this->resetAfterTest();
// Create one course with a group.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
$this->setUser($user);
// Our statement.
$statement = new statement();
// Populate statement.
if ($useagent) {
$statement->set_actor(item_agent::create_from_user($user));
} else {
$statement->set_actor(item_group::create_from_group($group));
}
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_activity::create_from_id('paella'));
foreach ($extras as $extra) {
$method = 'set_'.$extra;
$item = $this->get_valid_item($extra);
$statement->$method($item);
}
// For now extra values have no validation.
foreach ($extravalues as $extra) {
$method = 'set_'.$extra;
$statement->$method('Example');
}
// Check resulting statement.
if ($useagent) {
$stuser = $statement->get_user();
$this->assertEquals($user->id, $stuser->id);
$stusers = $statement->get_all_users();
$this->assertCount(1, $stusers);
} else {
$stgroup = $statement->get_group();
$this->assertEquals($group->id, $stgroup->id);
$stusers = $statement->get_all_users();
$this->assertCount(1, $stusers);
$stuser = array_shift($stusers);
$this->assertEquals($user->id, $stuser->id);
}
$this->assertEquals('cook', $statement->get_verb_id());
$this->assertEquals('paella', $statement->get_activity_id());
// Check resulting json (only first node structure, internal structure
// depends on every item json_encode test).
$data = json_decode(json_encode($statement));
$this->assertNotEmpty($data->actor);
$this->assertNotEmpty($data->verb);
$this->assertNotEmpty($data->object);
$allextras = ['context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'];
$alldefined = array_merge($extras, $extravalues);
foreach ($allextras as $extra) {
if (in_array($extra, $alldefined)) {
$this->assertObjectHasProperty($extra, $data);
$this->assertNotEmpty($data->$extra);
} else {
$this->assertObjectNotHasProperty($extra, $data);
}
}
}
/**
* Data provider for the test_create and test_create_from_data tests.
*
* @return array
*/
public function create_provider(): array {
return [
'Agent statement with no extras' => [
true, [], []
],
'Agent statement with context' => [
true, ['context'], []
],
'Agent statement with result' => [
true, ['result'], []
],
'Agent statement with timestamp' => [
true, [], ['timestamp']
],
'Agent statement with stored' => [
true, [], ['stored']
],
'Agent statement with authority' => [
true, ['authority'], []
],
'Agent statement with version' => [
true, [], ['version']
],
'Group statement with no extras' => [
false, [], []
],
'Group statement with context' => [
false, ['context'], []
],
'Group statement with result' => [
false, ['result'], []
],
'Group statement with timestamp' => [
false, [], ['timestamp']
],
'Group statement with stored' => [
false, [], ['stored']
],
'Group statement with authority' => [
false, ['authority'], []
],
'Group statement with version' => [
false, [], ['version']
],
];
}
/**
* Test statement creation from xAPI statement data.
*
* @dataProvider create_provider
* @param bool $useagent if use agent as actor (or group if false)
* @param array $extras extra item elements
* @param array $extravalues extra string values
*/
public function test_create_from_data(bool $useagent, array $extras, array $extravalues): void {
$this->resetAfterTest();
// Create one course with a group.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
$this->setUser($user);
// Populate data.
if ($useagent) {
$actor = item_agent::create_from_user($user);
} else {
$actor = item_group::create_from_group($group);
}
$verb = item_verb::create_from_id('cook');
$object = item_activity::create_from_id('paella');
$data = (object) [
'actor' => $actor->get_data(),
'verb' => $verb->get_data(),
'object' => $object->get_data(),
];
foreach ($extras as $extra) {
$item = $this->get_valid_item($extra);
$data->$extra = $item->get_data();
}
// For now extra values have no validation.
foreach ($extravalues as $extra) {
$data->$extra = 'Example';
}
$statement = statement::create_from_data($data);
// Check resulting statement.
if ($useagent) {
$stuser = $statement->get_user();
$this->assertEquals($user->id, $stuser->id);
$stusers = $statement->get_all_users();
$this->assertCount(1, $stusers);
} else {
$stgroup = $statement->get_group();
$this->assertEquals($group->id, $stgroup->id);
$stusers = $statement->get_all_users();
$this->assertCount(1, $stusers);
$stuser = array_shift($stusers);
$this->assertEquals($user->id, $stuser->id);
}
$this->assertEquals('cook', $statement->get_verb_id());
$this->assertEquals('paella', $statement->get_activity_id());
// Check resulting json (only first node structure, internal structure
// depends on every item json_encode test).
$data = json_decode(json_encode($statement));
$this->assertNotEmpty($data->actor);
$this->assertNotEmpty($data->verb);
$this->assertNotEmpty($data->object);
$allextras = ['context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'];
$alldefined = array_merge($extras, $extravalues);
foreach ($allextras as $extra) {
if (in_array($extra, $alldefined)) {
$this->assertObjectHasProperty($extra, $data);
$this->assertNotEmpty($data->object);
} else {
$this->assertObjectNotHasProperty($extra, $data);
}
}
}
/**
* Test adding attachments to statement.
*
*/
public function test_add_attachment(): void {
// Our statement.
$statement = new statement();
$attachments = $statement->get_attachments();
$this->assertNull($attachments);
$item = $this->get_valid_item('attachment');
$itemdata = $item->get_data();
$statement->add_attachment($item);
$attachments = $statement->get_attachments();
$this->assertNotNull($attachments);
$this->assertCount(1, $attachments);
$attachment = current($attachments);
$attachmentdata = $attachment->get_data();
$this->assertEquals($itemdata->usageType, $attachmentdata->usageType);
$this->assertEquals($itemdata->length, $attachmentdata->length);
// Check resulting json.
$statementdata = json_decode(json_encode($statement));
$this->assertObjectHasProperty('attachments', $statementdata);
$this->assertNotEmpty($statementdata->attachments);
$this->assertCount(1, $statementdata->attachments);
}
/**
* Test adding attachments to statement.
*
*/
public function test_add_attachment_from_data(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$actor = item_agent::create_from_user($user);
$verb = item_verb::create_from_id('cook');
$object = item_activity::create_from_id('paella');
$data = (object) [
'actor' => $actor->get_data(),
'verb' => $verb->get_data(),
'object' => $object->get_data(),
];
$item = $this->get_valid_item('attachment');
$itemdata = $item->get_data();
$data->attachments = [$itemdata];
$statement = statement::create_from_data($data);
$attachments = $statement->get_attachments();
$this->assertNotNull($attachments);
$this->assertCount(1, $attachments);
$attachment = current($attachments);
$attachmentdata = $attachment->get_data();
$this->assertEquals($itemdata->usageType, $attachmentdata->usageType);
$this->assertEquals($itemdata->length, $attachmentdata->length);
$statementdata = json_decode(json_encode($statement));
$this->assertObjectHasProperty('attachments', $statementdata);
$this->assertNotEmpty($statementdata->attachments);
$this->assertCount(1, $statementdata->attachments);
// Now try to send an invalid attachments.
$this->expectException(xapi_exception::class);
$data->attachments = 'Invalid data';
$statement = statement::create_from_data($data);
}
/**
* Test all getters into a not set statement.
*
* @dataProvider invalid_gets_provider
* @param string $method the method to test
* @param bool $exception if an exception is expected
*/
public function test_invalid_gets(string $method, bool $exception): void {
$statement = new statement();
if ($exception) {
$this->expectException(xapi_exception::class);
}
$result = $statement->$method();
$this->assertNull($result);
}
/**
* Data provider for the text_invalid_gets.
*
* @return array
*/
public function invalid_gets_provider(): array {
return [
'Method get_user on empty statement' => ['get_user', true],
'Method get_all_users on empty statement' => ['get_all_users', true],
'Method get_group on empty statement' => ['get_group', true],
'Method get_verb_id on empty statement' => ['get_verb_id', true],
'Method get_activity_id on empty statement' => ['get_activity_id', true],
'Method get_actor on empty statement' => ['get_actor', false],
'Method get_verb on empty statement' => ['get_verb', false],
'Method get_object on empty statement' => ['get_object', false],
'Method get_context on empty statement' => ['get_context', false],
'Method get_result on empty statement' => ['get_result', false],
'Method get_timestamp on empty statement' => ['get_timestamp', false],
'Method get_stored on empty statement' => ['get_stored', false],
'Method get_authority on empty statement' => ['get_authority', false],
'Method get_version on empty statement' => ['get_version', false],
'Method get_attachments on empty statement' => ['get_attachments', false],
];
}
/**
* Try to get a user from a group statement.
*/
public function test_invalid_get_user(): void {
$this->resetAfterTest();
// Create one course with a group.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
// Our statement.
$statement = new statement();
// Populate statement.
$statement->set_actor(item_group::create_from_group($group));
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_activity::create_from_id('paella'));
$this->expectException(xapi_exception::class);
$statement->get_user();
}
/**
* Try to get a group from an agent statement.
*/
public function test_invalid_get_group(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
// Our statement.
$statement = new statement();
// Populate statement.
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_activity::create_from_id('paella'));
$this->expectException(xapi_exception::class);
$statement->get_group();
}
/**
* Try to get activity Id from a statement with agent object.
*/
public function test_invalid_get_activity_id(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
// Our statement.
$statement = new statement();
// Populate statement with and agent object.
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_agent::create_from_user($user));
$this->expectException(xapi_exception::class);
$statement->get_activity_id();
}
/**
* Test for invalid structures.
*
* @dataProvider invalid_data_provider
* @param bool $useuser if use user into statement
* @param bool $userverb if use verb into statement
* @param bool $useobject if use object into statement
*/
public function test_invalid_data(bool $useuser, bool $userverb, bool $useobject): void {
$data = new stdClass();
if ($useuser) {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$data->actor = item_agent::create_from_user($user);
}
if ($userverb) {
$data->verb = item_verb::create_from_id('cook');
}
if ($useobject) {
$data->object = item_activity::create_from_id('paella');
}
$this->expectException(xapi_exception::class);
$statement = statement::create_from_data($data);
}
/**
* Data provider for the test_invalid_data tests.
*
* @return array
*/
public function invalid_data_provider(): array {
return [
'No actor, no verb, no object' => [false, false, false],
'No actor, verb, no object' => [false, true, false],
'No actor, no verb, object' => [false, false, true],
'No actor, verb, object' => [false, true, true],
'Actor, no verb, no object' => [true, false, false],
'Actor, verb, no object' => [true, true, false],
'Actor, no verb, object' => [true, false, true],
];
}
/**
* Test minify statement.
*/
public function test_minify(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Our statement.
$statement = new statement();
// Populate statement.
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('cook'));
$statement->set_object(item_activity::create_from_id('paella'));
$statement->set_result($this->get_valid_item('result'));
$statement->set_context($this->get_valid_item('context'));
$statement->set_authority($this->get_valid_item('authority'));
$statement->add_attachment($this->get_valid_item('attachment'));
$statement->set_version('Example');
$statement->set_timestamp('Example');
$statement->set_stored('Example');
$min = $statement->minify();
// Check calculated fields.
$this->assertCount(6, $min);
$this->assertArrayNotHasKey('actor', $min);
$this->assertArrayHasKey('verb', $min);
$this->assertArrayHasKey('object', $min);
$this->assertArrayHasKey('context', $min);
$this->assertArrayHasKey('result', $min);
$this->assertArrayNotHasKey('timestamp', $min);
$this->assertArrayNotHasKey('stored', $min);
$this->assertArrayHasKey('authority', $min);
$this->assertArrayNotHasKey('version', $min);
$this->assertArrayHasKey('attachments', $min);
}
}
+268
View File
@@ -0,0 +1,268 @@
<?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 core_xapi\privacy;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\transform;
use core_xapi\privacy\provider;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
/**
* Privacy tests for core_xapi.
*
* @package core_xapi
* @category test
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_xapi\privacy\provider
*/
class provider_test extends provider_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Helper to set up some sample data.
*
* @return array Array with the users that have been created.
*/
protected function set_up_data(): array {
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$user3 = self::getDataGenerator()->create_user();
// Add a few xAPI state records to database.
$context = \context_system::instance();
$cid = $context->id;
$this->setUser($user1);
test_helper::create_state(['activity' => item_activity::create_from_id($context->id)], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('3'), 'component' => 'mod_h5pactivity'], true);
$this->setUser($user2);
test_helper::create_state(['activity' => item_activity::create_from_id($context->id)], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5')], true);
$this->setUser($user3);
test_helper::create_state(['activity' => item_activity::create_from_id($cid), 'component' => 'mod_h5pactivity'], true);
return [$user1, $user2, $user3];
}
/**
* Test confirming that contexts of xapi items can be added to the contextlist.
*/
public function test_add_contexts_for_userid(): void {
$this->resetAfterTest();
// Scenario.
list($user1, $user2) = $this->set_up_data();
// Ask the xapi privacy api to export contexts for xapi of the type we just created, for user1.
$contextlist = new \core_privacy\local\request\contextlist();
provider::add_contexts_for_userid($contextlist, $user1->id, 'fake_component');
$this->assertCount(2, $contextlist->get_contextids());
$contextlist = new \core_privacy\local\request\contextlist();
provider::add_contexts_for_userid($contextlist, $user1->id, 'mod_h5pactivity');
$this->assertCount(1, $contextlist->get_contextids());
// Ask the xapi privacy api to export contexts for xapi of the type we just created, for user2.
$contextlist = new \core_privacy\local\request\contextlist();
provider::add_contexts_for_userid($contextlist, $user2->id, 'fake_component');
$this->assertCount(4, $contextlist->get_contextids());
$contextlist = new \core_privacy\local\request\contextlist();
provider::add_contexts_for_userid($contextlist, $user2->id, 'mod_h5pactivity');
$this->assertCount(0, $contextlist->get_contextids());
}
/**
* Test confirming that user ID's of xapi states can be added to the userlist.
*/
public function test_add_userids_for_context(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
list($user1, $user2, $user3) = $this->set_up_data();
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
$systemcontext = \context_system::instance();
// Ask the xapi privacy api to export userids for xapi states of the type we just created, in the system context.
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
provider::add_userids_for_context($userlist, 'fake_component');
// Only user1 and user2 should be returned, because user3 has a different component for the system context.
$this->assertCount(2, $userlist->get_userids());
$expected = [
$user1->id,
$user2->id,
];
$this->assertEqualsCanonicalizing($expected, $userlist->get_userids());
// Ask the xapi privacy api to export userids for xapi states of the type we just created for a different component.
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'mod_h5pactivity');
provider::add_userids_for_context($userlist, 'mod_h5pactivity');
// Only user3 should be returned, because the others have a different component for the system context.
$this->assertCount(1, $userlist->get_userids());
$expected = [$user3->id];
$this->assertEqualsCanonicalizing($expected, $userlist->get_userids());
// Ask the xapi privacy api to export userids xapi states for an empty component.
$userlist = new \core_privacy\local\request\userlist($systemcontext, 'empty_component');
provider::add_userids_for_context($userlist, 'empty_component');
$this->assertCount(0, $userlist->get_userids());
}
/**
* Test fetching the xapi state data for a specified user in a specified component and itemid.
*/
public function test_get_xapi_states_for_user(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
list($user1, $user2, $user3) = $this->set_up_data();
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
$systemcontext = \context_system::instance();
// Get the states info for user1 in the system context.
$result = provider::get_xapi_states_for_user($user1->id, 'fake_component', $systemcontext->id);
$info = (object) reset($result);
// Ensure the correct data has been returned.
$this->assertNotEmpty($info->statedata);
$this->assertNotEmpty(transform::datetime($info->timecreated));
$this->assertNotEmpty(transform::datetime($info->timemodified));
// Get the states info for user2 in the system context.
$result = provider::get_xapi_states_for_user($user2->id, 'fake_component', $systemcontext->id);
$info = (object) reset($result);
// Ensure the correct data has been returned.
$this->assertNotEmpty($info->statedata);
$this->assertNotEmpty(transform::datetime($info->timecreated));
$this->assertNotEmpty(transform::datetime($info->timemodified));
// Get the states info for user3 in the system context (it should be empty).
$info = provider::get_xapi_states_for_user($user3->id, 'fake_component', $systemcontext->id);
// Ensure the correct data has been returned.
$this->assertEmpty($info);
}
/**
* Test deletion of user xapi states based on an approved_contextlist and component area.
*/
public function test_delete_states_for_user(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
list($user1, $user2, $user3) = $this->set_up_data();
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
// Now, delete the xapistates for user1 only.
$user1context = \context_user::instance($user1->id);
$approvedcontextlist = new \core_privacy\local\request\approved_contextlist($user1, 'fake_component', [$user1context->id]);
provider::delete_states_for_user($approvedcontextlist, 'fake_component');
// Verify that we have no xapi states for user1 for the fake_component but that the rest of records are intact.
$this->assertEquals(0, $DB->count_records('xapi_states', ['userid' => $user1->id, 'component' => 'fake_component']));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user1->id, 'component' => 'mod_h5pactivity']));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
}
/**
* Test deletion of all user xapi states.
*/
public function test_delete_states_for_all_users(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
list($user1, $user2, $user3) = $this->set_up_data();
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
// Now, delete all course module xapi states in the 'fake_component' context only.
provider::delete_states_for_all_users(\context_system::instance(), 'fake_component');
// Verify that only content with the context_system for the fake_component have been removed.
$this->assertEquals(2, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
}
/**
* Test deletion of user xapi states based on an approved_userlist and component area.
*/
public function test_delete_states_for_userlist(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
list($user1, $user2, $user3) = $this->set_up_data();
$this->assertEquals(3, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(4, $DB->count_records('xapi_states', ['userid' => $user2->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
$systemcontext = \context_system::instance();
// Ask the xapi privacy api to export userids for states of the type we just created, in the system context.
$userlist1 = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
provider::add_userids_for_context($userlist1);
// Verify we have two userids in the list for system context.
$this->assertCount(2, $userlist1->get_userids());
// Now, delete the states for user1 only in the system context.
$approveduserlist = new \core_privacy\local\request\approved_userlist($systemcontext, 'fake_component', [$user1->id]);
provider::delete_states_for_userlist($approveduserlist);
// Ensure user1's data was deleted and user2 is still returned for system context.
$userlist1 = new \core_privacy\local\request\userlist($systemcontext, 'fake_component');
provider::add_userids_for_context($userlist1);
$this->assertCount(1, $userlist1->get_userids());
// Verify that user2 is still in the list for system context.
$expected = [$user2->id];
$this->assertEquals($expected, $userlist1->get_userids());
// Verify that the data of user1 in other contexts was not deleted.
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user3->id]));
$this->assertEquals(1, $DB->count_records('xapi_states', ['userid' => $user1->id]));
$this->assertEquals(2, $DB->count_records('xapi_states', ['itemid' => $systemcontext->id]));
// Verify that no data is removed if the component is empty.
$userlist3 = new \core_privacy\local\request\userlist($systemcontext, 'empty_component');
provider::add_userids_for_context($userlist3);
$this->assertCount(0, $userlist3->get_userids());
$this->assertEquals(2, $DB->count_records('xapi_states', ['itemid' => $systemcontext->id]));
}
}
+614
View File
@@ -0,0 +1,614 @@
<?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 core_xapi;
use core_xapi\local\statement\item_agent;
use core_xapi\local\statement\item_activity;
use advanced_testcase;
/**
* Contains test cases for testing xAPI state store methods.
*
* @package core_xapi
* @since Moodle 4.2
* @covers \core_xapi\state_store
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class state_store_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Testing delete method.
*
* @dataProvider states_provider
* @param array $info Array of overriden state data.
* @param bool $expected Expected results.
* @return void
*/
public function test_state_store_delete(array $info, bool $expected): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// Perform test.
$component = $info['component'] ?? 'fake_component';
$state = test_helper::create_state($info);
$store = new state_store($component);
$result = $store->delete($state);
// Check the state has been removed.
$records = $DB->get_records('xapi_states');
$this->assertTrue($result);
if ($expected) {
$this->assertCount($currentstates - 1, $records);
} else if ($expected === 'false') {
$this->assertCount($currentstates, $records);
}
}
/**
* Testing get method.
*
* @dataProvider states_provider
* @param array $info Array of overriden state data.
* @param bool $expected Expected results.
* @return void
*/
public function test_state_store_get(array $info, bool $expected): void {
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Perform test.
$component = $info['component'] ?? 'fake_component';
$state = test_helper::create_state($info);
// Remove statedata from the state object, to guarantee the get method is working as expected.
$state->set_state_data(null);
$store = new state_store($component);
$result = $store->get($state);
// Check the returned state has the expected values.
if ($expected) {
$this->assertEquals(json_encode($state->jsonSerialize()), json_encode($result->jsonSerialize()));
} else {
$this->assertNull($result);
}
}
/**
* Data provider for the test_state_store_delete and test_state_store_get tests.
*
* @return array
*/
public function states_provider(): array {
return [
'Existing and valid state' => [
'info' => [],
'expected' => true,
],
'No state (wrong activityid)' => [
'info' => ['activity' => item_activity::create_from_id('1')],
'expected' => false,
],
'No state (wrong stateid)' => [
'info' => ['stateid' => 'food'],
'expected' => false,
],
'No state (wrong component)' => [
'info' => ['component' => 'mod_h5pactivity'],
'expected' => false,
],
];
}
/**
* Testing put method.
*
* @dataProvider put_states_provider
* @param array $info Array of overriden state data.
* @param string $expected Expected results.
* @return void
*/
public function test_state_store_put(array $info, string $expected): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add, at least, one xAPI state record to database (with the default values).
test_helper::create_state([], true);
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// Perform test.
$component = $info['component'] ?? 'fake_component';
$state = test_helper::create_state($info);
$store = new state_store($component);
$result = $store->put($state);
// Check the state has been added/updated.
$this->assertTrue($result);
$recordsnum = $DB->count_records('xapi_states');
$params = [
'component' => $component,
'userid' => $state->get_user()->id,
'itemid' => $state->get_activity_id(),
'stateid' => $state->get_state_id(),
'registration' => $state->get_registration(),
];
$records = $DB->get_records('xapi_states', $params);
$record = reset($records);
if ($expected === 'added') {
$this->assertEquals($currentstates + 1, $recordsnum);
$this->assertEquals($record->timecreated, $record->timemodified);
} else if ($expected === 'updated') {
$this->assertEquals($currentstates, $recordsnum);
$this->assertGreaterThanOrEqual($record->timecreated, $record->timemodified);
}
$this->assertEquals($component, $record->component);
$this->assertEquals($state->get_activity_id(), $record->itemid);
$this->assertEquals($state->get_user()->id, $record->userid);
$this->assertEquals(json_encode($state->jsonSerialize()), $record->statedata);
$this->assertEquals($state->get_registration(), $record->registration);
}
/**
* Data provider for the test_state_store_put tests.
*
* @return array
*/
public function put_states_provider(): array {
return [
'Update existing state' => [
'info' => [],
'expected' => 'updated',
],
'Update existing state (change statedata)' => [
'info' => ['statedata' => '{"progress":0,"answers":[[["BB"],[""]],[{"answers":[]}]],"answered":[true,false]}'],
'expected' => 'updated',
],
'Add state (with different itemid)' => [
'info' => ['activity' => item_activity::create_from_id('1')],
'expected' => 'added',
],
'Add state (with different stateid)' => [
'info' => ['stateid' => 'food'],
'expected' => 'added',
],
'Add state (with different component)' => [
'info' => ['component' => 'mod_h5pactivity'],
'expected' => 'added',
],
];
}
/**
* Testing reset method.
*
* @dataProvider reset_wipe_states_provider
* @param array $info Array of overriden state data.
* @param int $expected The states that will be reset.
* @return void
*/
public function test_state_store_reset(array $info, int $expected): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('3'),
'agent' => item_agent::create_from_user($other),
'stateid' => 'paella',
'registration' => 'ABC',
], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('4'),
'agent' => item_agent::create_from_user($other),
], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('6'),
'component' => 'my_component',
'stateid' => 'paella',
'agent' => item_agent::create_from_user($other),
], true);
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// Perform test.
$component = $info['component'] ?? 'fake_component';
$itemid = $info['activity'] ?? null;
$userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
$stateid = $info['stateid'] ?? null;
$registration = $info['registration'] ?? null;
$store = new state_store($component);
$store->reset($itemid, $userid, $stateid, $registration);
// Check the states haven't been removed.
$this->assertCount($currentstates, $DB->get_records('xapi_states'));
$records = $DB->get_records_select('xapi_states', 'statedata IS NULL');
$this->assertCount($expected, $records);
}
/**
* Testing wipe method.
*
* @dataProvider reset_wipe_states_provider
* @param array $info Array of overriden state data.
* @param int $expected The removed states.
* @return void
*/
public function test_state_store_wipe(array $info, int $expected): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2'), 'stateid' => 'paella'], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('3'),
'agent' => item_agent::create_from_user($other),
'stateid' => 'paella',
'registration' => 'ABC',
], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('4'),
'agent' => item_agent::create_from_user($other),
], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
test_helper::create_state([
'activity' => item_activity::create_from_id('6'),
'component' => 'my_component',
'stateid' => 'paella',
'agent' => item_agent::create_from_user($other),
], true);
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// Perform test.
$component = $info['component'] ?? 'fake_component';
$itemid = $info['activity'] ?? null;
$userid = (array_key_exists('agent', $info) && $info['agent'] === 'other') ? $other->id : null;
$stateid = $info['stateid'] ?? null;
$registration = $info['registration'] ?? null;
$store = new state_store($component);
$store->wipe($itemid, $userid, $stateid, $registration);
// Check the states have been removed.
$records = $DB->get_records('xapi_states');
$this->assertCount($currentstates - $expected, $records);
}
/**
* Data provider for the test_state_store_reset and test_state_store_wipe tests.
*
* @return array
*/
public function reset_wipe_states_provider(): array {
return [
'With fake_component' => [
'info' => [],
'expected' => 4,
],
'With my_component' => [
'info' => ['component' => 'my_component'],
'expected' => 2,
],
'With unexisting_component' => [
'info' => ['component' => 'unexisting_component'],
'expected' => 0,
],
'Existing activity' => [
'info' => ['activity' => '1'],
'expected' => 1,
],
'Unexisting activity' => [
'info' => ['activity' => '1111'],
'expected' => 0,
],
'Existing userid' => [
'info' => ['agent' => 'other'],
'expected' => 2,
],
'Existing stateid' => [
'info' => ['stateid' => 'paella'],
'expected' => 2,
],
'Unexisting stateid' => [
'info' => ['stateid' => 'chorizo'],
'expected' => 0,
],
'Existing registration' => [
'info' => ['registration' => 'ABC'],
'expected' => 1,
],
'Uxexisting registration' => [
'info' => ['registration' => 'XYZ'],
'expected' => 0,
],
'Existing stateid combined with activity' => [
'info' => ['activity' => '3', 'stateid' => 'paella'],
'expected' => 1,
],
'Uxexisting stateid combined with activity' => [
'info' => ['activity' => '1', 'stateid' => 'paella'],
'expected' => 0,
],
];
}
/**
* Testing cleanup method.
*
* @return void
*/
public function test_state_store_cleanup(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'my_component'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'my_component'], true);
// Get current states in database.
$currentstates = $DB->count_records('xapi_states');
// Perform test.
$component = 'fake_component';
$store = new state_store($component);
$store->cleanup();
// Check no state has been removed (because the entries are not old enough).
$this->assertEquals($currentstates, $DB->count_records('xapi_states'));
// Make the existing state entries older.
$timepast = time() - 2;
$DB->set_field('xapi_states', 'timecreated', $timepast);
$DB->set_field('xapi_states', 'timemodified', $timepast);
// Create 1 more state, that shouldn't be removed after the cleanup.
test_helper::create_state(['activity' => item_activity::create_from_id('7')], true);
// Set the config to remove states older than 1 second.
set_config('xapicleanupperiod', 1);
// Check old states for fake_component have been removed.
$currentstates = $DB->count_records('xapi_states');
$store->cleanup();
$this->assertEquals($currentstates - 4, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => $component]));
$this->assertEquals(2, $DB->count_records('xapi_states', ['component' => 'my_component']));
}
/**
* Testing get_state_ids method.
*
* @dataProvider get_state_ids_provider
* @param string $component
* @param string|null $itemid
* @param string|null $registration
* @param bool|null $since
* @param array $expected the expected result
* @return void
*/
public function test_get_state_ids(
string $component,
?string $itemid,
?string $registration,
?bool $since,
array $expected,
): void {
global $DB, $USER;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
$other = $this->getDataGenerator()->create_user();
// Add a few xAPI state records to database.
$states = [
['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'],
['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'bb'],
['activity' => item_activity::create_from_id('1'), 'registration' => 'reg2', 'stateid' => 'cc'],
['activity' => item_activity::create_from_id('2'), 'registration' => 'reg', 'stateid' => 'dd'],
['activity' => item_activity::create_from_id('3'), 'stateid' => 'ee'],
['activity' => item_activity::create_from_id('4'), 'component' => 'other', 'stateid' => 'ff'],
];
foreach ($states as $state) {
test_helper::create_state($state, true);
}
// Make all existing state entries older except form two.
$currenttime = time();
$timepast = $currenttime - 5;
$DB->set_field('xapi_states', 'timecreated', $timepast);
$DB->set_field('xapi_states', 'timemodified', $timepast);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'aa']);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'bb']);
$DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'dd']);
// Perform test.
$sincetime = ($since) ? $currenttime - 1 : null;
$store = new state_store($component);
$stateids = $store->get_state_ids($itemid, $USER->id, $registration, $sincetime);
sort($stateids);
$this->assertEquals($expected, $stateids);
}
/**
* Data provider for the test_get_state_ids.
*
* @return array
*/
public function get_state_ids_provider(): array {
return [
'empty_component' => [
'component' => 'empty_component',
'itemid' => null,
'registration' => null,
'since' => null,
'expected' => [],
],
'filter_by_itemid' => [
'component' => 'fake_component',
'itemid' => '1',
'registration' => null,
'since' => null,
'expected' => ['aa', 'bb', 'cc'],
],
'filter_by_registration' => [
'component' => 'fake_component',
'itemid' => null,
'registration' => 'reg',
'since' => null,
'expected' => ['bb', 'dd'],
],
'filter_by_since' => [
'component' => 'fake_component',
'itemid' => null,
'registration' => null,
'since' => true,
'expected' => ['aa', 'bb', 'dd'],
],
'filter_by_itemid_and_registration' => [
'component' => 'fake_component',
'itemid' => '1',
'registration' => 'reg',
'since' => null,
'expected' => ['bb'],
],
'filter_by_itemid_registration_since' => [
'component' => 'fake_component',
'itemid' => '1',
'registration' => 'reg',
'since' => true,
'expected' => ['bb'],
],
'filter_by_registration_since' => [
'component' => 'fake_component',
'itemid' => null,
'registration' => 'reg',
'since' => true,
'expected' => ['bb', 'dd'],
],
];
}
/**
* Test delete with a non numeric activity id.
*
* The default state store only allows integer itemids.
*
* @dataProvider invalid_activityid_format_provider
* @param string $operation the method to execute
* @param bool $usestate if the param is a state or the activity id
*/
public function test_invalid_activityid_format(string $operation, bool $usestate = false): void {
$this->resetAfterTest();
$this->setAdminUser();
$state = test_helper::create_state([
'activity' => item_activity::create_from_id('notnumeric'),
]);
$param = ($usestate) ? $state : 'notnumeric';
$this->expectException(xapi_exception::class);
$store = new state_store('fake_component');
$store->$operation($param);
}
/**
* Data provider for test_invalid_activityid_format.
*
* @return array
*/
public function invalid_activityid_format_provider(): array {
return [
'delete' => [
'operation' => 'delete',
'usestate' => true,
],
'get' => [
'operation' => 'get',
'usestate' => true,
],
'put' => [
'operation' => 'put',
'usestate' => true,
],
'reset' => [
'operation' => 'reset',
'usestate' => false,
],
'wipe' => [
'operation' => 'wipe',
'usestate' => false,
],
'get_state_ids' => [
'operation' => 'get_state_ids',
'usestate' => false,
],
];
}
}
@@ -0,0 +1,87 @@
<?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 core_xapi\task;
use core_xapi\local\statement\item_activity;
use advanced_testcase;
use core_xapi\test_helper;
/**
* Contains test cases for testing the scheduled task state_cleanup_task.
*
* @package core_xapi
* @since Moodle 4.2
* @covers \core_xapi\task\state_cleanup_task
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class state_cleanup_task_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Testing execute method in state_cleanup_task.
*/
public function test_state_cleanup_task(): void {
global $DB;
$this->resetAfterTest();
// Scenario.
$this->setAdminUser();
// Add a few xAPI state records to database.
test_helper::create_state(['activity' => item_activity::create_from_id('1')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('2')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('3')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('4')], true);
test_helper::create_state(['activity' => item_activity::create_from_id('5'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('6'), 'component' => 'mod_h5pactivity'], true);
test_helper::create_state(['activity' => item_activity::create_from_id('7'), 'component' => 'mod_h5pactivity'], true);
// Perform test.
$task = new state_cleanup_task();
$task->execute();
// Check no state has been removed yet (because the entries are not old enough).
$this->assertEquals(7, $DB->count_records('xapi_states'));
// Make the existing state entries older.
$timepast = time() - 2;
$DB->set_field('xapi_states', 'timecreated', $timepast);
$DB->set_field('xapi_states', 'timemodified', $timepast);
// Create 1 more state, that shouldn't be removed after the cleanup.
test_helper::create_state(['activity' => item_activity::create_from_id('8'), 'component' => 'mod_h5pactivity'], true);
// Set the config to remove states older than 1 second.
set_config('xapicleanupperiod', 1);
// Check old states have been removed.
$task->execute();
$this->assertEquals(5, $DB->count_records('xapi_states'));
$this->assertEquals(4, $DB->count_records('xapi_states', ['component' => 'fake_component']));
$this->assertEquals(1, $DB->count_records('xapi_states', ['component' => 'mod_h5pactivity']));
$this->assertEquals(0, $DB->count_records('xapi_states', ['component' => 'my_component']));
}
}