Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
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_courseformat; 18 19 use moodle_exception; 20 use stdClass; 21 22 /** 23 * Tests for the stateactions class. 24 * 25 * @package core_courseformat 26 * @category test 27 * @copyright 2021 Sara Arjona (sara@moodle.com) 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 * @coversDefaultClass \core_courseformat\stateactions 30 */ 31 class stateactions_test extends \advanced_testcase { 32 33 /** 34 * Helper method to create an activity into a section and add it to the $sections and $activities arrays. 35 * 36 * @param int $courseid Course identifier where the activity will be added. 37 * @param string $type Activity type ('forum', 'assign', ...). 38 * @param int $section Section number where the activity will be added. 39 * @param bool $visible Whether the activity will be visible or not. 40 * @return int the activity cm id 41 */ 42 private function create_activity( 43 int $courseid, 44 string $type, 45 int $section, 46 bool $visible = true 47 ): int { 48 49 $activity = $this->getDataGenerator()->create_module( 50 $type, 51 ['course' => $courseid], 52 [ 53 'section' => $section, 54 'visible' => $visible 55 ] 56 ); 57 return $activity->cmid; 58 } 59 60 /** 61 * Helper to create a course and generate a section list. 62 * 63 * @param string $format the course format 64 * @param int $sections the number of sections 65 * @param int[] $hiddensections the section numbers to hide 66 * @return stdClass the course object 67 */ 68 private function create_course(string $format, int $sections, array $hiddensections): stdClass { 69 global $DB; 70 71 $course = $this->getDataGenerator()->create_course(['numsections' => $sections, 'format' => $format]); 72 foreach ($hiddensections as $section) { 73 set_section_visible($course->id, $section, 0); 74 } 75 76 return $course; 77 } 78 79 /** 80 * Return an array if the course references. 81 * 82 * This method is used to create alias to sections and other stuff in the dataProviders. 83 * 84 * @param stdClass $course the course object 85 * @return int[] a relation betwee all references and its element id 86 */ 87 private function course_references(stdClass $course): array { 88 global $DB; 89 90 $references = []; 91 92 $sectionrecords = $DB->get_records('course_sections', ['course' => $course->id]); 93 foreach ($sectionrecords as $id => $section) { 94 $references["section{$section->section}"] = $section->id; 95 } 96 $references['course'] = $course->id; 97 $references['invalidsection'] = -1; 98 $references['invalidcm'] = -1; 99 100 return $references; 101 } 102 103 /** 104 * Translate a references array into current ids. 105 * 106 * @param string[] $references the references list 107 * @param string[] $values the values to translate 108 * @return int[] the list of ids 109 */ 110 private function translate_references(array $references, array $values): array { 111 $result = []; 112 foreach ($values as $value) { 113 $result[] = $references[$value]; 114 } 115 return $result; 116 } 117 118 /** 119 * Generate a sorted and summarized list of an state updates message. 120 * 121 * It is important to note that the order in the update messages are not important in a real scenario 122 * because each message affects a specific part of the course state. However, for the PHPUnit test 123 * have them sorted and classified simplifies the asserts. 124 * 125 * @param stateupdates $updateobj the state updates object 126 * @return array of all data updates. 127 */ 128 private function summarize_updates(stateupdates $updateobj): array { 129 // Check state returned after executing given action. 130 $updatelist = $updateobj->jsonSerialize(); 131 132 // Initial summary structure. 133 $result = [ 134 'create' => [ 135 'course' => [], 136 'section' => [], 137 'cm' => [], 138 'count' => 0, 139 ], 140 'put' => [ 141 'course' => [], 142 'section' => [], 143 'cm' => [], 144 'count' => 0, 145 ], 146 'remove' => [ 147 'course' => [], 148 'section' => [], 149 'cm' => [], 150 'count' => 0, 151 ], 152 ]; 153 foreach ($updatelist as $update) { 154 if (!isset($result[$update->action])) { 155 $result[$update->action] = [ 156 'course' => [], 157 'section' => [], 158 'cm' => [], 159 'count' => 0, 160 ]; 161 } 162 $elementid = $update->fields->id ?? 0; 163 $result[$update->action][$update->name][$elementid] = $update->fields; 164 $result[$update->action]['count']++; 165 } 166 return $result; 167 } 168 169 /** 170 * Test the behaviour course_state. 171 * 172 * @dataProvider get_state_provider 173 * @covers ::course_state 174 * @covers ::section_state 175 * @covers ::cm_state 176 * 177 * @param string $format The course will be created with this course format. 178 * @param string $role The role of the user that will execute the method. 179 * @param string $method the method to call 180 * @param array $params the ids, targetsection and targetcm to use as params 181 * @param array $expectedresults List of the course module names expected after calling the method. 182 * @param bool $expectedexception If this call will raise an exception. 183 */ 184 public function test_get_state( 185 string $format, 186 string $role, 187 string $method, 188 array $params, 189 array $expectedresults, 190 bool $expectedexception = false 191 ): void { 192 193 $this->resetAfterTest(); 194 195 // Create a course with 3 sections, 1 of them hidden. 196 $course = $this->create_course($format, 3, [2]); 197 198 $references = $this->course_references($course); 199 200 // Create and enrol user using given role. 201 if ($role == 'admin') { 202 $this->setAdminUser(); 203 } else { 204 $user = $this->getDataGenerator()->create_user(); 205 if ($role != 'unenroled') { 206 $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); 207 } 208 $this->setUser($user); 209 } 210 211 // Add some activities to the course. One visible and one hidden in both sections 1 and 2. 212 $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true); 213 $references["cm1"] = $this->create_activity($course->id, 'book', 1, false); 214 $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true); 215 $references["cm3"] = $this->create_activity($course->id, 'page', 2, false); 216 217 if ($expectedexception) { 218 $this->expectException(moodle_exception::class); 219 } 220 221 // Initialise stateupdates. 222 $courseformat = course_get_format($course->id); 223 $updates = new stateupdates($courseformat); 224 225 // Execute given method. 226 $actions = new stateactions(); 227 $actions->$method( 228 $updates, 229 $course, 230 $this->translate_references($references, $params['ids']), 231 $references[$params['targetsectionid']] ?? null, 232 $references[$params['targetcmid']] ?? null 233 ); 234 235 // Format results in a way we can compare easily. 236 $results = $this->summarize_updates($updates); 237 238 // The state actions does not use create or remove actions because they are designed 239 // to refresh parts of the state. 240 $this->assertEquals(0, $results['create']['count']); 241 $this->assertEquals(0, $results['remove']['count']); 242 243 // Validate we have all the expected entries. 244 $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']); 245 $this->assertEquals($expectedtotal, $results['put']['count']); 246 247 // Validate course, section and cm. 248 foreach ($expectedresults as $name => $referencekeys) { 249 foreach ($referencekeys as $referencekey) { 250 $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]); 251 } 252 } 253 } 254 255 /** 256 * Data provider for data request creation tests. 257 * 258 * @return array the testing scenarios 259 */ 260 public function get_state_provider(): array { 261 return array_merge( 262 $this->course_state_provider('weeks'), 263 $this->course_state_provider('topics'), 264 $this->course_state_provider('social'), 265 $this->section_state_provider('weeks', 'admin'), 266 $this->section_state_provider('weeks', 'editingteacher'), 267 $this->section_state_provider('weeks', 'student'), 268 $this->section_state_provider('topics', 'admin'), 269 $this->section_state_provider('topics', 'editingteacher'), 270 $this->section_state_provider('topics', 'student'), 271 $this->section_state_provider('social', 'admin'), 272 $this->section_state_provider('social', 'editingteacher'), 273 $this->section_state_provider('social', 'student'), 274 $this->cm_state_provider('weeks', 'admin'), 275 $this->cm_state_provider('weeks', 'editingteacher'), 276 $this->cm_state_provider('weeks', 'student'), 277 $this->cm_state_provider('topics', 'admin'), 278 $this->cm_state_provider('topics', 'editingteacher'), 279 $this->cm_state_provider('topics', 'student'), 280 $this->cm_state_provider('social', 'admin'), 281 $this->cm_state_provider('social', 'editingteacher'), 282 $this->cm_state_provider('social', 'student'), 283 ); 284 } 285 286 /** 287 * Course state data provider. 288 * 289 * @param string $format the course format 290 * @return array the testing scenarios 291 */ 292 public function course_state_provider(string $format): array { 293 $expectedexception = ($format === 'social'); 294 return [ 295 // Tests for course_state. 296 "admin $format course_state" => [ 297 'format' => $format, 298 'role' => 'admin', 299 'method' => 'course_state', 300 'params' => [ 301 'ids' => [], 'targetsectionid' => null, 'targetcmid' => null 302 ], 303 'expectedresults' => [ 304 'course' => ['course'], 305 'section' => ['section0', 'section1', 'section2', 'section3'], 306 'cm' => ['cm0', 'cm1', 'cm2', 'cm3'], 307 ], 308 'expectedexception' => $expectedexception, 309 ], 310 "editingteacher $format course_state" => [ 311 'format' => $format, 312 'role' => 'editingteacher', 313 'method' => 'course_state', 314 'params' => [ 315 'ids' => [], 'targetsectionid' => null, 'targetcmid' => null 316 ], 317 'expectedresults' => [ 318 'course' => ['course'], 319 'section' => ['section0', 'section1', 'section2', 'section3'], 320 'cm' => ['cm0', 'cm1', 'cm2', 'cm3'], 321 ], 322 'expectedexception' => $expectedexception, 323 ], 324 "student $format course_state" => [ 325 'format' => $format, 326 'role' => 'student', 327 'method' => 'course_state', 328 'params' => [ 329 'ids' => [], 'targetsectionid' => null, 'targetcmid' => null 330 ], 331 'expectedresults' => [ 332 'course' => ['course'], 333 'section' => ['section0', 'section1', 'section3'], 334 'cm' => ['cm0'], 335 ], 336 'expectedexception' => $expectedexception, 337 ], 338 ]; 339 } 340 341 /** 342 * Section state data provider. 343 * 344 * @param string $format the course format 345 * @param string $role the user role 346 * @return array the testing scenarios 347 */ 348 public function section_state_provider(string $format, string $role): array { 349 350 // Social format will raise an exception and debug messages because it does not 351 // use sections and it does not provide a renderer. 352 $expectedexception = ($format === 'social'); 353 354 // All sections and cms that the user can access to. 355 $usersections = ['section0', 'section1', 'section2', 'section3']; 356 $usercms = ['cm0', 'cm1', 'cm2', 'cm3']; 357 if ($role == 'student') { 358 $usersections = ['section0', 'section1', 'section3']; 359 $usercms = ['cm0']; 360 } 361 362 return [ 363 "$role $format section_state no section" => [ 364 'format' => $format, 365 'role' => $role, 366 'method' => 'section_state', 367 'params' => [ 368 'ids' => [], 'targetsectionid' => null, 'targetcmid' => null 369 ], 370 'expectedresults' => [], 371 'expectedexception' => true, 372 ], 373 "$role $format section_state section 0" => [ 374 'format' => $format, 375 'role' => $role, 376 'method' => 'section_state', 377 'params' => [ 378 'ids' => ['section0'], 'targetsectionid' => null, 'targetcmid' => null 379 ], 380 'expectedresults' => [ 381 'course' => [], 382 'section' => array_intersect(['section0'], $usersections), 383 'cm' => [], 384 ], 385 'expectedexception' => $expectedexception, 386 ], 387 "$role $format section_state visible section" => [ 388 'format' => $format, 389 'role' => $role, 390 'method' => 'section_state', 391 'params' => [ 392 'ids' => ['section1'], 'targetsectionid' => null, 'targetcmid' => null 393 ], 394 'expectedresults' => [ 395 'course' => [], 396 'section' => array_intersect(['section1'], $usersections), 397 'cm' => array_intersect(['cm0', 'cm1'], $usercms), 398 ], 399 'expectedexception' => $expectedexception, 400 ], 401 "$role $format section_state hidden section" => [ 402 'format' => $format, 403 'role' => $role, 404 'method' => 'section_state', 405 'params' => [ 406 'ids' => ['section2'], 'targetsectionid' => null, 'targetcmid' => null 407 ], 408 'expectedresults' => [ 409 'course' => [], 410 'section' => array_intersect(['section2'], $usersections), 411 'cm' => array_intersect(['cm2', 'cm3'], $usercms), 412 ], 413 'expectedexception' => $expectedexception, 414 ], 415 "$role $format section_state several sections" => [ 416 'format' => $format, 417 'role' => $role, 418 'method' => 'section_state', 419 'params' => [ 420 'ids' => ['section1', 'section3'], 'targetsectionid' => null, 'targetcmid' => null 421 ], 422 'expectedresults' => [ 423 'course' => [], 424 'section' => array_intersect(['section1', 'section3'], $usersections), 425 'cm' => array_intersect(['cm0', 'cm1'], $usercms), 426 ], 427 'expectedexception' => $expectedexception, 428 ], 429 "$role $format section_state invalid section" => [ 430 'format' => $format, 431 'role' => $role, 432 'method' => 'section_state', 433 'params' => [ 434 'ids' => ['invalidsection'], 'targetsectionid' => null, 'targetcmid' => null 435 ], 436 'expectedresults' => [], 437 'expectedexception' => true, 438 ], 439 "$role $format section_state using target section" => [ 440 'format' => $format, 441 'role' => $role, 442 'method' => 'section_state', 443 'params' => [ 444 'ids' => ['section1'], 'targetsectionid' => 'section3', 'targetcmid' => null 445 ], 446 'expectedresults' => [ 447 'course' => [], 448 'section' => array_intersect(['section1', 'section3'], $usersections), 449 'cm' => array_intersect(['cm0', 'cm1'], $usercms), 450 ], 451 'expectedexception' => $expectedexception, 452 ], 453 "$role $format section_state using target targetcmid" => [ 454 'format' => $format, 455 'role' => $role, 456 'method' => 'section_state', 457 'params' => [ 458 'ids' => ['section3'], 'targetsectionid' => null, 'targetcmid' => 'cm1' 459 ], 460 'expectedresults' => [ 461 'course' => [], 462 'section' => array_intersect(['section3'], $usersections), 463 'cm' => array_intersect(['cm1'], $usercms), 464 ], 465 'expectedexception' => $expectedexception, 466 ], 467 ]; 468 } 469 470 /** 471 * Course module state data provider. 472 * 473 * @param string $format the course format 474 * @param string $role the user role 475 * @return array the testing scenarios 476 */ 477 public function cm_state_provider(string $format, string $role): array { 478 479 // All sections and cms that the user can access to. 480 $usersections = ['section0', 'section1', 'section2', 'section3']; 481 $usercms = ['cm0', 'cm1', 'cm2', 'cm3']; 482 if ($role == 'student') { 483 $usersections = ['section0', 'section1', 'section3']; 484 $usercms = ['cm0']; 485 } 486 487 return [ 488 "$role $format cm_state no cms" => [ 489 'format' => $format, 490 'role' => $role, 491 'method' => 'cm_state', 492 'params' => [ 493 'ids' => [], 'targetsectionid' => null, 'targetcmid' => null 494 ], 495 'expectedresults' => [], 496 'expectedexception' => true, 497 ], 498 "$role $format cm_state visible cm" => [ 499 'format' => $format, 500 'role' => $role, 501 'method' => 'cm_state', 502 'params' => [ 503 'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => null 504 ], 505 'expectedresults' => [ 506 'course' => [], 507 'section' => array_intersect(['section1'], $usersections), 508 'cm' => array_intersect(['cm0'], $usercms), 509 ], 510 'expectedexception' => false, 511 ], 512 "$role $format cm_state hidden cm" => [ 513 'format' => $format, 514 'role' => $role, 515 'method' => 'cm_state', 516 'params' => [ 517 'ids' => ['cm1'], 'targetsectionid' => null, 'targetcmid' => null 518 ], 519 'expectedresults' => [ 520 'course' => [], 521 'section' => array_intersect(['section1'], $usersections), 522 'cm' => array_intersect(['cm1'], $usercms), 523 ], 524 'expectedexception' => false, 525 ], 526 "$role $format cm_state several cm" => [ 527 'format' => $format, 528 'role' => $role, 529 'method' => 'cm_state', 530 'params' => [ 531 'ids' => ['cm0', 'cm2'], 'targetsectionid' => null, 'targetcmid' => null 532 ], 533 'expectedresults' => [ 534 'course' => [], 535 'section' => array_intersect(['section1', 'section2'], $usersections), 536 'cm' => array_intersect(['cm0', 'cm2'], $usercms), 537 ], 538 'expectedexception' => false, 539 ], 540 "$role $format cm_state using targetsection" => [ 541 'format' => $format, 542 'role' => $role, 543 'method' => 'cm_state', 544 'params' => [ 545 'ids' => ['cm0'], 'targetsectionid' => 'section2', 'targetcmid' => null 546 ], 547 'expectedresults' => [ 548 'course' => [], 549 'section' => array_intersect(['section1', 'section2'], $usersections), 550 'cm' => array_intersect(['cm0'], $usercms), 551 ], 552 'expectedexception' => ($format === 'social'), 553 ], 554 "$role $format cm_state using targetcm" => [ 555 'format' => $format, 556 'role' => $role, 557 'method' => 'cm_state', 558 'params' => [ 559 'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => 'cm3' 560 ], 561 'expectedresults' => [ 562 'course' => [], 563 'section' => array_intersect(['section1', 'section2'], $usersections), 564 'cm' => array_intersect(['cm0', 'cm3'], $usercms), 565 ], 566 'expectedexception' => false, 567 ], 568 "$role $format cm_state using an invalid cm" => [ 569 'format' => $format, 570 'role' => $role, 571 'method' => 'cm_state', 572 'params' => [ 573 'ids' => ['invalidcm'], 'targetsectionid' => null, 'targetcmid' => null 574 ], 575 'expectedresults' => [], 576 'expectedexception' => true, 577 ], 578 ]; 579 } 580 581 /** 582 * Internal method for testing a specific state action. 583 * 584 * @param string $method the method to test 585 * @param string $role the user role 586 * @param string[] $idrefs the sections or cms id references to be used as method params 587 * @param bool $expectedexception whether the call should throw an exception 588 * @param int $expectedtotal the expected total number of state puts 589 * @param string|null $coursefield the course field to check 590 * @param int|string|null $coursevalue the section field value 591 * @param string|null $sectionfield the section field to check 592 * @param int|string|null $sectionvalue the section field value 593 * @param string|null $cmfield the cm field to check 594 * @param int|string|null $cmvalue the cm field value 595 * @return array the state update summary 596 */ 597 protected function basic_state_text( 598 string $method = 'section_hide', 599 string $role = 'editingteacher', 600 array $idrefs = [], 601 bool $expectedexception = false, 602 int $expectedtotal = 0, 603 ?string $coursefield = null, 604 $coursevalue = 0, 605 ?string $sectionfield = null, 606 $sectionvalue = 0, 607 ?string $cmfield = null, 608 $cmvalue = 0 609 ): array { 610 $this->resetAfterTest(); 611 612 // Create a course with 3 sections, 1 of them hidden. 613 $course = $this->create_course('topics', 3, [2]); 614 615 $references = $this->course_references($course); 616 617 $user = $this->getDataGenerator()->create_user(); 618 $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); 619 $this->setUser($user); 620 621 // Add some activities to the course. One visible and one hidden in both sections 1 and 2. 622 $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true); 623 $references["cm1"] = $this->create_activity($course->id, 'book', 1, false); 624 $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true); 625 $references["cm3"] = $this->create_activity($course->id, 'page', 2, false); 626 627 if ($expectedexception) { 628 $this->expectException(moodle_exception::class); 629 } 630 631 // Initialise stateupdates. 632 $courseformat = course_get_format($course->id); 633 $updates = new stateupdates($courseformat); 634 635 // Execute the method. 636 $actions = new stateactions(); 637 $actions->$method( 638 $updates, 639 $course, 640 $this->translate_references($references, $idrefs), 641 ); 642 643 // Format results in a way we can compare easily. 644 $results = $this->summarize_updates($updates); 645 646 // Most state actions does not use create or remove actions because they are designed 647 // to refresh parts of the state. 648 $this->assertEquals(0, $results['create']['count']); 649 $this->assertEquals(0, $results['remove']['count']); 650 651 // Validate we have all the expected entries. 652 $this->assertEquals($expectedtotal, $results['put']['count']); 653 654 // Validate course, section and cm. 655 if (!empty($coursefield)) { 656 foreach ($results['put']['course'] as $courseid) { 657 $this->assertEquals($coursevalue, $results['put']['course'][$courseid][$coursefield]); 658 } 659 } 660 if (!empty($sectionfield)) { 661 foreach ($results['put']['section'] as $section) { 662 $this->assertEquals($sectionvalue, $section->$sectionfield); 663 } 664 } 665 if (!empty($cmfield)) { 666 foreach ($results['put']['cm'] as $cm) { 667 $this->assertEquals($cmvalue, $cm->$cmfield); 668 } 669 } 670 return $results; 671 } 672 673 /** 674 * Data provider for basic role tests. 675 * 676 * @return array the testing scenarios 677 */ 678 public function basic_role_provider() { 679 return [ 680 'editingteacher' => [ 681 'role' => 'editingteacher', 682 'expectedexception' => false, 683 ], 684 'teacher' => [ 685 'role' => 'teacher', 686 'expectedexception' => true, 687 ], 688 'student' => [ 689 'role' => 'student', 690 'expectedexception' => true, 691 ], 692 'guest' => [ 693 'role' => 'guest', 694 'expectedexception' => true, 695 ], 696 ]; 697 } 698 699 /** 700 * Test for cm_moveright 701 * 702 * @covers ::cm_moveright 703 * @dataProvider basic_role_provider 704 * @param string $role the user role 705 * @param bool $expectedexception if it will expect an exception. 706 */ 707 public function test_cm_moveright( 708 string $role = 'editingteacher', 709 bool $expectedexception = false 710 ): void { 711 $this->basic_state_text( 712 'cm_moveright', 713 $role, 714 ['cm0', 'cm1', 'cm2', 'cm3'], 715 $expectedexception, 716 4, 717 null, 718 null, 719 null, 720 null, 721 'indent', 722 1 723 ); 724 } 725 726 /** 727 * Test for cm_moveleft 728 * 729 * @covers ::cm_moveleft 730 * @dataProvider basic_role_provider 731 * @param string $role the user role 732 * @param bool $expectedexception if it will expect an exception. 733 */ 734 public function test_cm_moveleft( 735 string $role = 'editingteacher', 736 bool $expectedexception = false 737 ): void { 738 $this->basic_state_text( 739 'cm_moveleft', 740 $role, 741 ['cm0', 'cm1', 'cm2', 'cm3'], 742 $expectedexception, 743 4, 744 null, 745 null, 746 null, 747 null, 748 'indent', 749 0 750 ); 751 } 752 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body