1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core_xapi\external; 18 19 use core\context\module; 20 use core_external\external_api; 21 use core_xapi\iri; 22 use core_xapi\local\statement\item_activity; 23 use core_xapi\local\statement\item_agent; 24 use core_xapi\test_helper; 25 use core_xapi\xapi_exception; 26 use externallib_advanced_testcase; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 global $CFG; 31 require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 32 33 /** 34 * Unit tests for xAPI delete states webservice. 35 * 36 * @package core_xapi 37 * @covers \core_xapi\external\delete_states 38 * @since Moodle 4.3 39 * @copyright 2023 Laurent David <laurent.david@moodle.com> 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 */ 42 class delete_states_test extends externallib_advanced_testcase { 43 44 /** 45 * Setup to ensure that fixtures are loaded. 46 */ 47 public static function setUpBeforeClass(): void { 48 global $CFG; 49 require_once($CFG->dirroot . '/lib/xapi/tests/helper.php'); 50 } 51 52 /** 53 * Testing different component names on valid states. 54 * 55 * @dataProvider components_provider 56 * @param string $component component name 57 * @param object|null $expected expected results 58 */ 59 public function test_component_names(string $component, ?object $expected): void { 60 global $DB, $USER; 61 $this->resetAfterTest(); 62 63 // Scenario. 64 $this->setAdminUser(); 65 66 // Perform test. 67 $info = [ 68 'agent' => item_agent::create_from_user($USER), 69 'activity' => item_activity::create_from_id('12345'), 70 ]; 71 test_helper::create_state($info, true); 72 if (!empty($expected->exception)) { 73 $this->expectException($expected->exception); 74 } 75 $this->execute($component, 76 iri::generate($info['activity']->get_id(), 'activity'), 77 json_encode($info['agent']) 78 ); 79 80 if (isset($expected->expectedcount)) { 81 $this->assertEquals($expected->expectedcount, $DB->record_exists('xapi_states', [])); 82 } 83 } 84 85 /** 86 * This function execute the delete_states_data 87 * 88 * @param string $component component name 89 * @param string $activityiri 90 * @param string $agent 91 * @param string|null $registration 92 * @return array empty array 93 */ 94 private function execute(string $component, 95 string $activityiri, 96 string $agent, 97 ?string $registration = null 98 ): void { 99 $external = $this->get_external_class(); 100 $external::execute( 101 $component, 102 $activityiri, 103 $agent, 104 $registration 105 ); 106 } 107 108 /** 109 * Return a xAPI external webservice class to operate. 110 * 111 * The test needs to fake a component in order to test without 112 * using a real one. This way if in the future any component 113 * implement it's xAPI handler this test will continue working. 114 * 115 * @return delete_states the external class 116 */ 117 private function get_external_class(): delete_states { 118 $ws = new class extends delete_states { 119 /** 120 * Method to override validate_component. 121 * 122 * @param string $component The component name in frankenstyle. 123 */ 124 protected static function validate_component(string $component): void { 125 if ($component != 'fake_component') { 126 parent::validate_component($component); 127 } 128 } 129 }; 130 return $ws; 131 } 132 133 /** 134 * Data provider for the test_component_names tests. 135 * 136 * @return array 137 */ 138 public function components_provider(): array { 139 return [ 140 'Inexistent component' => [ 141 'component' => 'inexistent_component', 142 'expected' => (object) ['exception' => xapi_exception::class], 143 ], 144 'Compatible component' => [ 145 'component' => 'fake_component', 146 'expected' => (object) ['expectedcount' => 0], 147 ], 148 'Incompatible component' => [ 149 'component' => 'core_xapi', 150 'expected' => (object) ['exception' => xapi_exception::class], 151 ], 152 ]; 153 } 154 155 /** 156 * Testing invalid agent. 157 * 158 */ 159 public function test_invalid_agent(): void { 160 $this->resetAfterTest(); 161 162 // Scenario. 163 $this->setAdminUser(); 164 $other = $this->getDataGenerator()->create_user(); 165 166 // Invalid agent (use different user, instead of the current one). 167 $info = [ 168 'agent' => item_agent::create_from_user($other), 169 'activity' => item_activity::create_from_id('12345'), 170 ]; 171 test_helper::create_state($info, true); 172 $this->expectException(xapi_exception::class); 173 $this->execute( 174 'fake_component', 175 iri::generate($info['activity']->get_id(), 'activity'), 176 json_encode($info['agent']) 177 ); 178 } 179 180 /** 181 * Testing deleting states 182 * 183 * @dataProvider states_provider 184 * @param string $testedusername 185 * @param string $testedcomponent 186 * @param string $testedactivityname 187 * @param array $states 188 * @param array $expectedstates 189 * @return void 190 */ 191 public function test_delete_states(string $testedusername, 192 string $testedcomponent, 193 string $testedactivityname, 194 array $states, 195 array $expectedstates 196 ): void { 197 global $DB; 198 $this->resetAfterTest(); 199 200 // Scenario. 201 $this->setAdminUser(); 202 $course = $this->getDataGenerator()->create_course(); 203 204 $activities = []; 205 $users = []; 206 // Create a set of states for different users and components. 207 foreach ($states as $stateinfo) { 208 $params = [ 209 'component' => $stateinfo['component'] ?? 'mod_h5pactivity', 210 ]; 211 $uname = $stateinfo['user']; 212 $user = $users[$uname] ?? null; 213 if (empty($user)) { 214 $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); 215 $users[$uname] = $user; 216 } 217 $activityname = $stateinfo['activity']; 218 $activity = $activities[$activityname] ?? null; 219 if (empty($activity)) { 220 if (empty($stateinfo['activityid'])) { 221 $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); 222 $activitycontext = module::instance($activity->cmid); 223 $activities[$activityname] = item_activity::create_from_id($activitycontext->id); 224 } else { 225 $activities[$activityname] = item_activity::create_from_id($stateinfo['activityid']); 226 } 227 } 228 $params['activity'] = $activities[$activityname]; 229 $params['agent'] = item_agent::create_from_user($user); 230 test_helper::create_state($params, true); 231 } 232 if (empty($users[$testedusername])) { 233 $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); 234 $users[$testedusername] = $user; 235 } 236 $this->setUser($users[$testedusername]); 237 $activity = $activities[$testedactivityname]; 238 $activityiri = iri::generate($activity->get_id(), 'activity'); 239 $agent = json_encode(item_agent::create_from_user($users[$testedusername])); 240 $this->execute($testedcomponent, 241 $activityiri, 242 $agent); 243 244 $statesleft = $DB->get_records('xapi_states'); 245 // Check that we have the expected leftover records. 246 $this->assertCount(count($expectedstates), $statesleft); 247 foreach ($expectedstates as $expectedstate) { 248 $expectedactivityid = $activities[$expectedstate['activity']]->get_id(); 249 $expecteduserid = $users[$expectedstate['user']]->id; 250 $found = false; 251 foreach ($statesleft as $state) { 252 if ($state->userid == $expecteduserid && $state->itemid == $expectedactivityid) { 253 $found = true; 254 break; 255 } 256 } 257 $this->assertTrue($found, 'State not found:' . json_encode($statesleft)); 258 } 259 } 260 261 /** 262 * Data provider for the test_get_state tests. 263 * 264 * @return array 265 */ 266 public function states_provider(): array { 267 return [ 268 'Activities with different users and components' => [ 269 'username' => 'user1', 270 'component' => 'mod_h5pactivity', 271 'activity' => 'Activity 1', 272 'states' => [ 273 [ 274 'user' => 'user1', 275 'activity' => 'Activity 1', 276 'component' => 'mod_h5pactivity' 277 ], 278 [ 279 'user' => 'user2', 280 'activity' => 'Activity 1', 281 'component' => 'mod_h5pactivity' 282 ], 283 [ 284 'user' => 'user1', 285 'activity' => 'Activity 3', 286 'activityid' => '1', 287 'component' => 'core_xapi' 288 ], 289 [ 290 'user' => 'user1', 291 'activity' => 'Activity 1', 292 'component' => 'mod_h5pactivity' 293 ], 294 ], 295 'expectedstatesleft' => [ 296 ['user' => 'user2', 'activity' => 'Activity 1'], 297 ['user' => 'user1', 'activity' => 'Activity 3'] 298 ] 299 ], 300 'Activities with one single user' => [ 301 'username' => 'user1', 302 'component' => 'mod_h5pactivity', 303 'activity' => 'Activity 1', 304 'states' => [ 305 [ 306 'user' => 'user1', 307 'activity' => 'Activity 1', 308 'component' => 'mod_h5pactivity' 309 ], 310 [ 311 'user' => 'user1', 312 'activity' => 'Activity 1', 313 'component' => 'mod_h5pactivity' 314 ], 315 [ 316 'user' => 'user1', 317 'activity' => 'Activity 1', 318 'component' => 'mod_h5pactivity' 319 ], 320 ], 321 'expectedstatesleft' => [] 322 ], 323 ]; 324 } 325 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body