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_xapi\xapi_exception; 20 use core_xapi\local\statement\item_agent; 21 use externallib_advanced_testcase; 22 use core_external\external_api; 23 use core_xapi\iri; 24 use core_xapi\local\state; 25 use core_xapi\local\statement\item_activity; 26 use core_xapi\test_helper; 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 get states webservice. 35 * 36 * @package core_xapi 37 * @covers \core_xapi\external\get_states 38 * @since Moodle 4.2 39 * @copyright 2023 Ferran Recio <ferran@moodle.com> 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 */ 42 class get_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 * Execute the get_states service from a generate state. 54 * 55 * @param string $component component name 56 * @param state $data the original state to extract the params 57 * @param string|null $since the formated timestamp or ISO 8601 date 58 * @param array $override overridden params 59 * @return string[] array of state ids 60 */ 61 private function execute_service( 62 string $component, 63 state $data, 64 ?string $since = null, 65 array $override = [] 66 ): array { 67 // Apply overrides. 68 $activityiri = $override['activityiri'] ?? iri::generate($data->get_activity_id(), 'activity'); 69 $registration = $override['registration'] ?? $data->get_registration(); 70 $agent = $override['agent'] ?? $data->get_agent(); 71 if (!empty($override['user'])) { 72 $agent = item_agent::create_from_user($override['user']); 73 } 74 75 $external = $this->get_external_class(); 76 $result = $external::execute( 77 $component, 78 $activityiri, 79 json_encode($agent), 80 $registration, 81 $since 82 ); 83 $result = external_api::clean_returnvalue($external::execute_returns(), $result); 84 85 // Sorting result to make them comparable. 86 sort($result); 87 return $result; 88 } 89 90 /** 91 * Return a xAPI external webservice class to operate. 92 * 93 * The test needs to fake a component in order to test without 94 * using a real one. This way if in the future any component 95 * implement it's xAPI handler this test will continue working. 96 * 97 * @return get_states the external class 98 */ 99 private function get_external_class(): get_states { 100 $ws = new class extends get_states { 101 /** 102 * Method to override validate_component. 103 * 104 * @param string $component The component name in frankenstyle. 105 */ 106 protected static function validate_component(string $component): void { 107 if ($component != 'fake_component') { 108 parent::validate_component($component); 109 } 110 } 111 }; 112 return $ws; 113 } 114 115 /** 116 * Testing different component names on valid states. 117 * 118 * @dataProvider components_provider 119 * @param string $component component name 120 * @param string|null $exception expect exception 121 */ 122 public function test_component_names(string $component, ?bool $exception): void { 123 $this->resetAfterTest(); 124 125 // Scenario. 126 $this->setAdminUser(); 127 128 // Add, at least, one xAPI state record to database. 129 $data = test_helper::create_state( 130 ['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'], 131 true 132 ); 133 134 // If no result is expected we will just incur in exception. 135 if ($exception) { 136 $this->expectException(xapi_exception::class); 137 } 138 139 $result = $this->execute_service($component, $data); 140 $this->assertEquals(['aa'], $result); 141 } 142 143 /** 144 * Data provider for the test_component_names tests. 145 * 146 * @return array 147 */ 148 public function components_provider() : array { 149 return [ 150 'Inexistent component' => [ 151 'component' => 'inexistent_component', 152 'exception' => true, 153 ], 154 'Compatible component' => [ 155 'component' => 'fake_component', 156 'exception' => false, 157 ], 158 'Incompatible component' => [ 159 'component' => 'core_xapi', 160 'exception' => true, 161 ], 162 ]; 163 } 164 165 /** 166 * Testing different since date formats. 167 * 168 * @dataProvider since_formats_provider 169 * @param string|null $since the formatted timestamps 170 * @param string[]|null $expected expected results 171 * @param bool $exception expect exception 172 */ 173 public function test_since_formats(?string $since, ?array $expected, bool $exception = false): void { 174 $this->resetAfterTest(); 175 $this->setAdminUser(); 176 177 $states = $this->generate_states(); 178 179 if ($exception) { 180 $this->expectException(xapi_exception::class); 181 } 182 183 $result = $this->execute_service('fake_component', $states['aa'], $since); 184 $this->assertEquals($expected, $result); 185 } 186 187 /** 188 * Data provider for the test_since_formats tests. 189 * 190 * @return array 191 */ 192 public function since_formats_provider(): array { 193 return [ 194 'Null date' => [ 195 'since' => null, 196 'expected' => ['aa', 'bb', 'cc', 'dd'], 197 'exception' => false, 198 ], 199 'Numeric timestamp' => [ 200 'since' => '1651100399', 201 'expected' => ['aa', 'bb'], 202 'exception' => false, 203 ], 204 'ISO 8601 format 1' => [ 205 'since' => '2022-04-28T06:59', 206 'expected' => ['aa', 'bb'], 207 'exception' => false, 208 ], 209 'ISO 8601 format 2' => [ 210 'since' => '2022-04-28T06:59:59', 211 'expected' => ['aa', 'bb'], 212 'exception' => false, 213 ], 214 'Wrong format' => [ 215 'since' => 'Spanish omelette without onion', 216 'expected' => null, 217 'exception' => true, 218 ], 219 ]; 220 } 221 222 /** 223 * Testing different activity IRI values. 224 * 225 * @dataProvider activity_iri_provider 226 * @param string|null $activityiri 227 * @param string[]|null $expected expected results 228 */ 229 public function test_activity_iri(?string $activityiri, ?array $expected): void { 230 $this->resetAfterTest(); 231 $this->setAdminUser(); 232 233 $states = $this->generate_states(); 234 235 $override = ['activityiri' => $activityiri]; 236 $result = $this->execute_service('fake_component', $states['aa'], null, $override); 237 $this->assertEquals($expected, $result); 238 } 239 240 /** 241 * Data provider for the test_activity_iri tests. 242 * 243 * @return array 244 */ 245 public function activity_iri_provider(): array { 246 return [ 247 'Activity with several states' => [ 248 'activityiri' => iri::generate('1', 'activity'), 249 'expected' => ['aa', 'bb', 'cc', 'dd'], 250 ], 251 'Activity with one state' => [ 252 'activityiri' => iri::generate('2', 'activity'), 253 'expected' => ['ee'], 254 ], 255 'Inexistent activity' => [ 256 'activityiri' => iri::generate('3', 'activity'), 257 'expected' => [], 258 ], 259 ]; 260 } 261 262 /** 263 * Testing different agent values. 264 * 265 * @dataProvider agent_values_provider 266 * @param string|null $agentreference the used agent reference 267 * @param string[]|null $expected expected results 268 * @param bool $exception expect exception 269 */ 270 public function test_agent_values(?string $agentreference, ?array $expected, bool $exception = false): void { 271 $this->resetAfterTest(); 272 $this->setAdminUser(); 273 274 $states = $this->generate_states(); 275 276 if ($exception) { 277 $this->expectException(xapi_exception::class); 278 } 279 280 $userreferences = [ 281 'current' => $states['aa']->get_user(), 282 'other' => $this->getDataGenerator()->create_user(), 283 ]; 284 285 $override = [ 286 'user' => $userreferences[$agentreference], 287 ]; 288 $result = $this->execute_service('fake_component', $states['aa'], null, $override); 289 $this->assertEquals($expected, $result); 290 } 291 292 /** 293 * Data provider for the test_agent_values tests. 294 * 295 * @return array 296 */ 297 public function agent_values_provider(): array { 298 return [ 299 'Current user' => [ 300 'agentreference' => 'current', 301 'expected' => ['aa', 'bb', 'cc', 'dd'], 302 'exception' => false, 303 ], 304 'Other user' => [ 305 'agentreference' => 'other', 306 'expected' => null, 307 'exception' => true, 308 ], 309 ]; 310 } 311 312 /** 313 * Testing different registration values. 314 * 315 * @dataProvider registration_values_provider 316 * @param string|null $registration 317 * @param string[]|null $expected expected results 318 */ 319 public function test_registration_values(?string $registration, ?array $expected): void { 320 $this->resetAfterTest(); 321 $this->setAdminUser(); 322 323 $states = $this->generate_states(); 324 325 $override = ['registration' => $registration]; 326 $result = $this->execute_service('fake_component', $states['aa'], null, $override); 327 $this->assertEquals($expected, $result); 328 } 329 330 /** 331 * Data provider for the test_registration_values tests. 332 * 333 * @return array 334 */ 335 public function registration_values_provider(): array { 336 return [ 337 'Null registration' => [ 338 'registration' => null, 339 'expected' => ['aa', 'bb', 'cc', 'dd'], 340 ], 341 'Registration with one state id' => [ 342 'registration' => 'reg2', 343 'expected' => ['cc'], 344 ], 345 'Registration with two state ids' => [ 346 'registration' => 'reg', 347 'expected' => ['bb', 'dd'], 348 ], 349 'Registration with no state ids' => [ 350 'registration' => 'invented', 351 'expected' => [], 352 ], 353 ]; 354 } 355 356 /** 357 * Generate the state for the testing scenarios. 358 * 359 * Generate a variaty of states from several components, registrations and state ids. 360 * Some of the states are registered as they are done in 27-04-2022 07:00:00 while others 361 * are updated in 28-04-2022 07:00:00. 362 * 363 * @return state[] 364 */ 365 private function generate_states(): array { 366 global $DB; 367 368 $testdate = \DateTime::createFromFormat('d-m-Y H:i:s', '28-04-2022 07:00:00'); 369 // Unix timestamp: 1651100400. 370 $currenttime = $testdate->getTimestamp(); 371 372 $result = []; 373 374 // Add a few xAPI state records to database. 375 $states = [ 376 ['activity' => item_activity::create_from_id('1'), 'stateid' => 'aa'], 377 ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'bb'], 378 ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg2', 'stateid' => 'cc'], 379 ['activity' => item_activity::create_from_id('1'), 'registration' => 'reg', 'stateid' => 'dd'], 380 ['activity' => item_activity::create_from_id('2'), 'stateid' => 'ee'], 381 ['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'stateid' => 'gg'], 382 ['activity' => item_activity::create_from_id('3'), 'component' => 'other', 'registration' => 'reg', 'stateid' => 'ff'], 383 ]; 384 foreach ($states as $state) { 385 $result[$state['stateid']] = test_helper::create_state($state, true); 386 } 387 388 $timepast = $currenttime - DAYSECS; 389 $DB->set_field('xapi_states', 'timecreated', $timepast); 390 $DB->set_field('xapi_states', 'timemodified', $timepast); 391 $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'aa']); 392 $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'bb']); 393 $DB->set_field('xapi_states', 'timemodified', $currenttime, ['stateid' => 'ee']); 394 395 return $result; 396 } 397 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body