Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 course_modinfo;
  20  use moodle_exception;
  21  use stdClass;
  22  
  23  /**
  24   * Tests for the stateactions class.
  25   *
  26   * @package    core_courseformat
  27   * @category   test
  28   * @copyright  2021 Sara Arjona (sara@moodle.com)
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   * @coversDefaultClass \core_courseformat\stateactions
  31   */
  32  class stateactions_test extends \advanced_testcase {
  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       * Enrol, set and create the test user depending on the role name.
 171       *
 172       * @param stdClass $course the course data
 173       * @param string $rolename the testing role name
 174       */
 175      private function set_test_user_by_role(stdClass $course, string $rolename) {
 176          if ($rolename == 'admin') {
 177              $this->setAdminUser();
 178          } else {
 179              $user = $this->getDataGenerator()->create_user();
 180              if ($rolename != 'unenroled') {
 181                  $this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
 182              }
 183              $this->setUser($user);
 184          }
 185      }
 186  
 187      /**
 188       * Test the behaviour course_state.
 189       *
 190       * @dataProvider get_state_provider
 191       * @covers ::course_state
 192       * @covers ::section_state
 193       * @covers ::cm_state
 194       *
 195       * @param string $format The course will be created with this course format.
 196       * @param string $role The role of the user that will execute the method.
 197       * @param string $method the method to call
 198       * @param array $params the ids, targetsection and targetcm to use as params
 199       * @param array $expectedresults List of the course module names expected after calling the method.
 200       * @param bool $expectedexception If this call will raise an exception.
 201  
 202       */
 203      public function test_get_state(
 204          string $format,
 205          string $role,
 206          string $method,
 207          array $params,
 208          array $expectedresults,
 209          bool $expectedexception = false
 210      ): void {
 211  
 212          $this->resetAfterTest();
 213  
 214          // Create a course with 3 sections, 1 of them hidden.
 215          $course = $this->create_course($format, 3, [2]);
 216  
 217          $references = $this->course_references($course);
 218  
 219          // Create and enrol user using given role.
 220          $this->set_test_user_by_role($course, $role);
 221  
 222          // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
 223          $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
 224          $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
 225          $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
 226          $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
 227  
 228          if ($expectedexception) {
 229              $this->expectException(moodle_exception::class);
 230          }
 231  
 232          // Initialise stateupdates.
 233          $courseformat = course_get_format($course->id);
 234          $updates = new stateupdates($courseformat);
 235  
 236          // Execute given method.
 237          $actions = new stateactions();
 238          $actions->$method(
 239              $updates,
 240              $course,
 241              $this->translate_references($references, $params['ids']),
 242              $references[$params['targetsectionid']] ?? null,
 243              $references[$params['targetcmid']] ?? null
 244          );
 245  
 246          // Format results in a way we can compare easily.
 247          $results = $this->summarize_updates($updates);
 248  
 249          // The state actions does not use create or remove actions because they are designed
 250          // to refresh parts of the state.
 251          $this->assertEquals(0, $results['create']['count']);
 252          $this->assertEquals(0, $results['remove']['count']);
 253  
 254          // Validate we have all the expected entries.
 255          $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']);
 256          $this->assertEquals($expectedtotal, $results['put']['count']);
 257  
 258          // Validate course, section and cm.
 259          foreach ($expectedresults as $name => $referencekeys) {
 260              foreach ($referencekeys as $referencekey) {
 261                  $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]);
 262              }
 263          }
 264      }
 265  
 266      /**
 267       * Data provider for data request creation tests.
 268       *
 269       * @return array the testing scenarios
 270       */
 271      public function get_state_provider(): array {
 272          return array_merge(
 273              $this->course_state_provider('weeks'),
 274              $this->course_state_provider('topics'),
 275              $this->course_state_provider('social'),
 276              $this->section_state_provider('weeks', 'admin'),
 277              $this->section_state_provider('weeks', 'editingteacher'),
 278              $this->section_state_provider('weeks', 'student'),
 279              $this->section_state_provider('topics', 'admin'),
 280              $this->section_state_provider('topics', 'editingteacher'),
 281              $this->section_state_provider('topics', 'student'),
 282              $this->section_state_provider('social', 'admin'),
 283              $this->section_state_provider('social', 'editingteacher'),
 284              $this->section_state_provider('social', 'student'),
 285              $this->cm_state_provider('weeks', 'admin'),
 286              $this->cm_state_provider('weeks', 'editingteacher'),
 287              $this->cm_state_provider('weeks', 'student'),
 288              $this->cm_state_provider('topics', 'admin'),
 289              $this->cm_state_provider('topics', 'editingteacher'),
 290              $this->cm_state_provider('topics', 'student'),
 291              $this->cm_state_provider('social', 'admin'),
 292              $this->cm_state_provider('social', 'editingteacher'),
 293              $this->cm_state_provider('social', 'student'),
 294          );
 295      }
 296  
 297      /**
 298       * Course state data provider.
 299       *
 300       * @param string $format the course format
 301       * @return array the testing scenarios
 302       */
 303      public function course_state_provider(string $format): array {
 304          $expectedexception = ($format === 'social');
 305          return [
 306              // Tests for course_state.
 307              "admin $format course_state" => [
 308                  'format' => $format,
 309                  'role' => 'admin',
 310                  'method' => 'course_state',
 311                  'params' => [
 312                      'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
 313                  ],
 314                  'expectedresults' => [
 315                      'course' => ['course'],
 316                      'section' => ['section0', 'section1', 'section2', 'section3'],
 317                      'cm' => ['cm0', 'cm1', 'cm2', 'cm3'],
 318                  ],
 319                  'expectedexception' => $expectedexception,
 320              ],
 321              "editingteacher $format course_state" => [
 322                  'format' => $format,
 323                  'role' => 'editingteacher',
 324                  'method' => 'course_state',
 325                  'params' => [
 326                      'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
 327                  ],
 328                  'expectedresults' => [
 329                      'course' => ['course'],
 330                      'section' => ['section0', 'section1', 'section2', 'section3'],
 331                      'cm' => ['cm0', 'cm1', 'cm2', 'cm3'],
 332                  ],
 333                  'expectedexception' => $expectedexception,
 334              ],
 335              "student $format course_state" => [
 336                  'format' => $format,
 337                  'role' => 'student',
 338                  'method' => 'course_state',
 339                  'params' => [
 340                      'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
 341                  ],
 342                  'expectedresults' => [
 343                      'course' => ['course'],
 344                      'section' => ['section0', 'section1', 'section3'],
 345                      'cm' => ['cm0'],
 346                  ],
 347                  'expectedexception' => $expectedexception,
 348              ],
 349          ];
 350      }
 351  
 352      /**
 353       * Section state data provider.
 354       *
 355       * @param string $format the course format
 356       * @param string $role the user role
 357       * @return array the testing scenarios
 358       */
 359      public function section_state_provider(string $format, string $role): array {
 360  
 361          // Social format will raise an exception and debug messages because it does not
 362          // use sections and it does not provide a renderer.
 363          $expectedexception = ($format === 'social');
 364  
 365          // All sections and cms that the user can access to.
 366          $usersections = ['section0', 'section1', 'section2', 'section3'];
 367          $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
 368          if ($role == 'student') {
 369              $usersections = ['section0', 'section1', 'section3'];
 370              $usercms = ['cm0'];
 371          }
 372  
 373          return [
 374              "$role $format section_state no section" => [
 375                  'format' => $format,
 376                  'role' => $role,
 377                  'method' => 'section_state',
 378                  'params' => [
 379                      'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
 380                  ],
 381                  'expectedresults' => [],
 382                  'expectedexception' => true,
 383              ],
 384              "$role $format section_state section 0" => [
 385                  'format' => $format,
 386                  'role' => $role,
 387                  'method' => 'section_state',
 388                  'params' => [
 389                      'ids' => ['section0'], 'targetsectionid' => null, 'targetcmid' => null
 390                  ],
 391                  'expectedresults' => [
 392                      'course' => [],
 393                      'section' => array_intersect(['section0'], $usersections),
 394                      'cm' => [],
 395                  ],
 396                  'expectedexception' => $expectedexception,
 397              ],
 398              "$role $format section_state visible section" => [
 399                  'format' => $format,
 400                  'role' => $role,
 401                  'method' => 'section_state',
 402                  'params' => [
 403                      'ids' => ['section1'], 'targetsectionid' => null, 'targetcmid' => null
 404                  ],
 405                  'expectedresults' => [
 406                      'course' => [],
 407                      'section' => array_intersect(['section1'], $usersections),
 408                      'cm' => array_intersect(['cm0', 'cm1'], $usercms),
 409                  ],
 410                  'expectedexception' => $expectedexception,
 411              ],
 412              "$role $format section_state hidden section" => [
 413                  'format' => $format,
 414                  'role' => $role,
 415                  'method' => 'section_state',
 416                  'params' => [
 417                      'ids' => ['section2'], 'targetsectionid' => null, 'targetcmid' => null
 418                  ],
 419                  'expectedresults' => [
 420                      'course' => [],
 421                      'section' => array_intersect(['section2'], $usersections),
 422                      'cm' => array_intersect(['cm2', 'cm3'], $usercms),
 423                  ],
 424                  'expectedexception' => $expectedexception,
 425              ],
 426              "$role $format section_state several sections" => [
 427                  'format' => $format,
 428                  'role' => $role,
 429                  'method' => 'section_state',
 430                  'params' => [
 431                      'ids' => ['section1', 'section3'], 'targetsectionid' => null, 'targetcmid' => null
 432                  ],
 433                  'expectedresults' => [
 434                      'course' => [],
 435                      'section' => array_intersect(['section1', 'section3'], $usersections),
 436                      'cm' => array_intersect(['cm0', 'cm1'], $usercms),
 437                  ],
 438                  'expectedexception' => $expectedexception,
 439              ],
 440              "$role $format section_state invalid section" => [
 441                  'format' => $format,
 442                  'role' => $role,
 443                  'method' => 'section_state',
 444                  'params' => [
 445                      'ids' => ['invalidsection'], 'targetsectionid' => null, 'targetcmid' => null
 446                  ],
 447                  'expectedresults' => [],
 448                  'expectedexception' => true,
 449              ],
 450              "$role $format section_state using target section" => [
 451                  'format' => $format,
 452                  'role' => $role,
 453                  'method' => 'section_state',
 454                  'params' => [
 455                      'ids' => ['section1'], 'targetsectionid' => 'section3', 'targetcmid' => null
 456                  ],
 457                  'expectedresults' => [
 458                      'course' => [],
 459                      'section' => array_intersect(['section1', 'section3'], $usersections),
 460                      'cm' => array_intersect(['cm0', 'cm1'], $usercms),
 461                  ],
 462                  'expectedexception' => $expectedexception,
 463              ],
 464              "$role $format section_state using target targetcmid" => [
 465                  'format' => $format,
 466                  'role' => $role,
 467                  'method' => 'section_state',
 468                  'params' => [
 469                      'ids' => ['section3'], 'targetsectionid' => null, 'targetcmid' => 'cm1'
 470                  ],
 471                  'expectedresults' => [
 472                      'course' => [],
 473                      'section' => array_intersect(['section3'], $usersections),
 474                      'cm' => array_intersect(['cm1'], $usercms),
 475                  ],
 476                  'expectedexception' => $expectedexception,
 477              ],
 478          ];
 479      }
 480  
 481      /**
 482       * Course module state data provider.
 483       *
 484       * @param string $format the course format
 485       * @param string $role the user role
 486       * @return array the testing scenarios
 487       */
 488      public function cm_state_provider(string $format, string $role): array {
 489  
 490          // All sections and cms that the user can access to.
 491          $usersections = ['section0', 'section1', 'section2', 'section3'];
 492          $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
 493          if ($role == 'student') {
 494              $usersections = ['section0', 'section1', 'section3'];
 495              $usercms = ['cm0'];
 496          }
 497  
 498          return [
 499              "$role $format cm_state no cms" => [
 500                  'format' => $format,
 501                  'role' => $role,
 502                  'method' => 'cm_state',
 503                  'params' => [
 504                      'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
 505                  ],
 506                  'expectedresults' => [],
 507                  'expectedexception' => true,
 508              ],
 509              "$role $format cm_state visible cm" => [
 510                  'format' => $format,
 511                  'role' => $role,
 512                  'method' => 'cm_state',
 513                  'params' => [
 514                      'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => null
 515                  ],
 516                  'expectedresults' => [
 517                      'course' => [],
 518                      'section' => array_intersect(['section1'], $usersections),
 519                      'cm' => array_intersect(['cm0'], $usercms),
 520                  ],
 521                  'expectedexception' => false,
 522              ],
 523              "$role $format cm_state hidden cm" => [
 524                  'format' => $format,
 525                  'role' => $role,
 526                  'method' => 'cm_state',
 527                  'params' => [
 528                      'ids' => ['cm1'], 'targetsectionid' => null, 'targetcmid' => null
 529                  ],
 530                  'expectedresults' => [
 531                      'course' => [],
 532                      'section' => array_intersect(['section1'], $usersections),
 533                      'cm' => array_intersect(['cm1'], $usercms),
 534                  ],
 535                  'expectedexception' => false,
 536              ],
 537              "$role $format cm_state several cm" => [
 538                  'format' => $format,
 539                  'role' => $role,
 540                  'method' => 'cm_state',
 541                  'params' => [
 542                      'ids' => ['cm0', 'cm2'], 'targetsectionid' => null, 'targetcmid' => null
 543                  ],
 544                  'expectedresults' => [
 545                      'course' => [],
 546                      'section' => array_intersect(['section1', 'section2'], $usersections),
 547                      'cm' => array_intersect(['cm0', 'cm2'], $usercms),
 548                  ],
 549                  'expectedexception' => false,
 550              ],
 551              "$role $format cm_state using targetsection" => [
 552                  'format' => $format,
 553                  'role' => $role,
 554                  'method' => 'cm_state',
 555                  'params' => [
 556                      'ids' => ['cm0'], 'targetsectionid' => 'section2', 'targetcmid' => null
 557                  ],
 558                  'expectedresults' => [
 559                      'course' => [],
 560                      'section' => array_intersect(['section1', 'section2'], $usersections),
 561                      'cm' => array_intersect(['cm0'], $usercms),
 562                  ],
 563                  'expectedexception' => ($format === 'social'),
 564              ],
 565              "$role $format cm_state using targetcm" => [
 566                  'format' => $format,
 567                  'role' => $role,
 568                  'method' => 'cm_state',
 569                  'params' => [
 570                      'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => 'cm3'
 571                  ],
 572                  'expectedresults' => [
 573                      'course' => [],
 574                      'section' => array_intersect(['section1', 'section2'], $usersections),
 575                      'cm' => array_intersect(['cm0', 'cm3'], $usercms),
 576                  ],
 577                  'expectedexception' => false,
 578              ],
 579              "$role $format cm_state using an invalid cm" => [
 580                  'format' => $format,
 581                  'role' => $role,
 582                  'method' => 'cm_state',
 583                  'params' => [
 584                      'ids' => ['invalidcm'], 'targetsectionid' => null, 'targetcmid' => null
 585                  ],
 586                  'expectedresults' => [],
 587                  'expectedexception' => true,
 588              ],
 589          ];
 590      }
 591  
 592      /**
 593       * Internal method for testing a specific state action.
 594       *
 595       * @param string $method the method to test
 596       * @param string $role the user role
 597       * @param string[] $idrefs the sections or cms id references to be used as method params
 598       * @param bool $expectedexception whether the call should throw an exception
 599       * @param int[] $expectedtotal the expected total number of state indexed by put, remove and create
 600       * @param string|null $coursefield the course field to check
 601       * @param int|string|null $coursevalue the section field value
 602       * @param string|null $sectionfield the section field to check
 603       * @param int|string|null $sectionvalue the section field value
 604       * @param string|null $cmfield the cm field to check
 605       * @param int|string|null $cmvalue the cm field value
 606       * @param string|null $targetsection optional target section reference
 607       * @param string|null $targetcm optional target cm reference
 608       * @return array an array of elements to do extra validations (course, references, results)
 609       */
 610      protected function basic_state_text(
 611          string $method = 'section_hide',
 612          string $role = 'editingteacher',
 613          array $idrefs = [],
 614          bool $expectedexception = false,
 615          array $expectedtotals = [],
 616          ?string $coursefield = null,
 617          $coursevalue = 0,
 618          ?string $sectionfield = null,
 619          $sectionvalue = 0,
 620          ?string $cmfield = null,
 621          $cmvalue = 0,
 622          ?string $targetsection = null,
 623          ?string $targetcm = null
 624      ): array {
 625          $this->resetAfterTest();
 626  
 627          // Create a course with 3 sections, 1 of them hidden.
 628          $course = $this->create_course('topics', 3, [2]);
 629  
 630          $references = $this->course_references($course);
 631  
 632          $user = $this->getDataGenerator()->create_user();
 633          $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
 634          $this->setUser($user);
 635  
 636          // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
 637          $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
 638          $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
 639          $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
 640          $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
 641          $references["cm4"] = $this->create_activity($course->id, 'forum', 2, false);
 642          $references["cm5"] = $this->create_activity($course->id, 'wiki', 2, false);
 643  
 644          if ($expectedexception) {
 645              $this->expectException(moodle_exception::class);
 646          }
 647  
 648          // Initialise stateupdates.
 649          $courseformat = course_get_format($course->id);
 650          $updates = new stateupdates($courseformat);
 651  
 652          // Execute the method.
 653          $actions = new stateactions();
 654          $actions->$method(
 655              $updates,
 656              $course,
 657              $this->translate_references($references, $idrefs),
 658              ($targetsection) ? $references[$targetsection] : null,
 659              ($targetcm) ? $references[$targetcm] : null,
 660          );
 661  
 662          // Format results in a way we can compare easily.
 663          $results = $this->summarize_updates($updates);
 664  
 665          // Validate we have all the expected entries.
 666          $this->assertEquals($expectedtotals['create'] ?? 0, $results['create']['count']);
 667          $this->assertEquals($expectedtotals['remove'] ?? 0, $results['remove']['count']);
 668          $this->assertEquals($expectedtotals['put'] ?? 0, $results['put']['count']);
 669  
 670          // Validate course, section and cm.
 671          if (!empty($coursefield)) {
 672              foreach ($results['put']['course'] as $courseid) {
 673                  $this->assertEquals($coursevalue, $results['put']['course'][$courseid][$coursefield]);
 674              }
 675          }
 676          if (!empty($sectionfield)) {
 677              foreach ($results['put']['section'] as $section) {
 678                  $this->assertEquals($sectionvalue, $section->$sectionfield);
 679              }
 680          }
 681          if (!empty($cmfield)) {
 682              foreach ($results['put']['cm'] as $cm) {
 683                  $this->assertEquals($cmvalue, $cm->$cmfield);
 684              }
 685          }
 686          return [
 687              'course' => $course,
 688              'references' => $references,
 689              'results' => $results,
 690          ];
 691      }
 692  
 693      /**
 694       * Test for section_hide
 695       *
 696       * @covers ::section_hide
 697       * @dataProvider basic_role_provider
 698       * @param string $role the user role
 699       * @param bool $expectedexception if it will expect an exception.
 700       */
 701      public function test_section_hide(
 702          string $role = 'editingteacher',
 703          bool $expectedexception = false
 704      ): void {
 705          $this->basic_state_text(
 706              'section_hide',
 707              $role,
 708              ['section1', 'section2', 'section3'],
 709              $expectedexception,
 710              ['put' => 9],
 711              null,
 712              null,
 713              'visible',
 714              0,
 715              null,
 716              null
 717          );
 718      }
 719  
 720      /**
 721       * Test for section_hide
 722       *
 723       * @covers ::section_show
 724       * @dataProvider basic_role_provider
 725       * @param string $role the user role
 726       * @param bool $expectedexception if it will expect an exception.
 727       */
 728      public function test_section_show(
 729          string $role = 'editingteacher',
 730          bool $expectedexception = false
 731      ): void {
 732          $this->basic_state_text(
 733              'section_show',
 734              $role,
 735              ['section1', 'section2', 'section3'],
 736              $expectedexception,
 737              ['put' => 9],
 738              null,
 739              null,
 740              'visible',
 741              1,
 742              null,
 743              null
 744          );
 745      }
 746  
 747      /**
 748       * Test for cm_show
 749       *
 750       * @covers ::cm_show
 751       * @dataProvider basic_role_provider
 752       * @param string $role the user role
 753       * @param bool $expectedexception if it will expect an exception.
 754       */
 755      public function test_cm_show(
 756          string $role = 'editingteacher',
 757          bool $expectedexception = false
 758      ): void {
 759          $this->basic_state_text(
 760              'cm_show',
 761              $role,
 762              ['cm0', 'cm1', 'cm2', 'cm3'],
 763              $expectedexception,
 764              ['put' => 4],
 765              null,
 766              null,
 767              null,
 768              null,
 769              'visible',
 770              1
 771          );
 772      }
 773  
 774      /**
 775       * Test for cm_hide
 776       *
 777       * @covers ::cm_hide
 778       * @dataProvider basic_role_provider
 779       * @param string $role the user role
 780       * @param bool $expectedexception if it will expect an exception.
 781       */
 782      public function test_cm_hide(
 783          string $role = 'editingteacher',
 784          bool $expectedexception = false
 785      ): void {
 786          $this->basic_state_text(
 787              'cm_hide',
 788              $role,
 789              ['cm0', 'cm1', 'cm2', 'cm3'],
 790              $expectedexception,
 791              ['put' => 4],
 792              null,
 793              null,
 794              null,
 795              null,
 796              'visible',
 797              0
 798          );
 799      }
 800  
 801      /**
 802       * Test for cm_stealth
 803       *
 804       * @covers ::cm_stealth
 805       * @dataProvider basic_role_provider
 806       * @param string $role the user role
 807       * @param bool $expectedexception if it will expect an exception.
 808       */
 809      public function test_cm_stealth(
 810          string $role = 'editingteacher',
 811          bool $expectedexception = false
 812      ): void {
 813          set_config('allowstealth', 1);
 814          $this->basic_state_text(
 815              'cm_stealth',
 816              $role,
 817              ['cm0', 'cm1', 'cm2', 'cm3'],
 818              $expectedexception,
 819              ['put' => 4],
 820              null,
 821              null,
 822              null,
 823              null,
 824              'stealth',
 825              1
 826          );
 827          // Disable stealth.
 828          set_config('allowstealth', 0);
 829          // When stealth are disabled the validation is a but more complex because they depends
 830          // also on the section visibility (legacy stealth).
 831          $this->basic_state_text(
 832              'cm_stealth',
 833              $role,
 834              ['cm0', 'cm1'],
 835              $expectedexception,
 836              ['put' => 2],
 837              null,
 838              null,
 839              null,
 840              null,
 841              'stealth',
 842              0
 843          );
 844          $this->basic_state_text(
 845              'cm_stealth',
 846              $role,
 847              ['cm2', 'cm3'],
 848              $expectedexception,
 849              ['put' => 2],
 850              null,
 851              null,
 852              null,
 853              null,
 854              'stealth',
 855              1
 856          );
 857      }
 858  
 859      /**
 860       * Data provider for basic role tests.
 861       *
 862       * @return array the testing scenarios
 863       */
 864      public function basic_role_provider() {
 865          return [
 866              'editingteacher' => [
 867                  'role' => 'editingteacher',
 868                  'expectedexception' => false,
 869              ],
 870              'teacher' => [
 871                  'role' => 'teacher',
 872                  'expectedexception' => true,
 873              ],
 874              'student' => [
 875                  'role' => 'student',
 876                  'expectedexception' => true,
 877              ],
 878              'guest' => [
 879                  'role' => 'guest',
 880                  'expectedexception' => true,
 881              ],
 882          ];
 883      }
 884  
 885      /**
 886       * Duplicate course module method.
 887       *
 888       * @covers ::cm_duplicate
 889       * @dataProvider cm_duplicate_provider
 890       * @param string $targetsection the target section (empty for none)
 891       * @param bool $validcms if uses valid cms
 892       * @param string $role the current user role name
 893       * @param bool $expectedexception if the test will raise an exception
 894       */
 895      public function test_cm_duplicate(
 896          string $targetsection = '',
 897          bool $validcms = true,
 898          string $role = 'admin',
 899          bool $expectedexception = false
 900      ) {
 901          $this->resetAfterTest();
 902  
 903          // Create a course with 3 sections.
 904          $course = $this->create_course('topics', 3, []);
 905  
 906          $references = $this->course_references($course);
 907  
 908          // Create and enrol user using given role.
 909          $this->set_test_user_by_role($course, $role);
 910  
 911          // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
 912          $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
 913          $references["cm1"] = $this->create_activity($course->id, 'page', 2, false);
 914  
 915          if ($expectedexception) {
 916              $this->expectException(moodle_exception::class);
 917          }
 918  
 919          // Initialise stateupdates.
 920          $courseformat = course_get_format($course->id);
 921          $updates = new stateupdates($courseformat);
 922  
 923          // Execute method.
 924          $targetsectionid = (!empty($targetsection)) ? $references[$targetsection] : null;
 925          $cmrefs = ($validcms) ? ['cm0', 'cm1'] : ['invalidcm'];
 926          $actions = new stateactions();
 927          $actions->cm_duplicate(
 928              $updates,
 929              $course,
 930              $this->translate_references($references, $cmrefs),
 931              $targetsectionid,
 932          );
 933  
 934          // Check the new elements in the course structure.
 935          $originalsections = [
 936              'assign' => $references['section1'],
 937              'page' => $references['section2'],
 938          ];
 939          $modinfo = course_modinfo::instance($course);
 940          $cms = $modinfo->get_cms();
 941          $i = 0;
 942          foreach ($cms as $cmid => $cminfo) {
 943              if ($cmid == $references['cm0'] || $cmid == $references['cm1']) {
 944                  continue;
 945              }
 946              $references["newcm$i"] = $cmid;
 947              if ($targetsectionid) {
 948                  $this->assertEquals($targetsectionid, $cminfo->section);
 949              } else {
 950                  $this->assertEquals($originalsections[$cminfo->modname], $cminfo->section);
 951              }
 952              $i++;
 953          }
 954  
 955          // Check the resulting updates.
 956          $results = $this->summarize_updates($updates);
 957  
 958          if ($targetsectionid) {
 959              $this->assertArrayHasKey($references[$targetsection], $results['put']['section']);
 960          } else {
 961              $this->assertArrayHasKey($references['section1'], $results['put']['section']);
 962              $this->assertArrayHasKey($references['section2'], $results['put']['section']);
 963          }
 964          $countcms = ($targetsection == 'section3' || $targetsection === '') ? 2 : 3;
 965          $this->assertCount($countcms, $results['put']['cm']);
 966          $this->assertArrayHasKey($references['newcm0'], $results['put']['cm']);
 967          $this->assertArrayHasKey($references['newcm1'], $results['put']['cm']);
 968      }
 969  
 970      /**
 971       * Duplicate course module data provider.
 972       *
 973       * @return array the testing scenarios
 974       */
 975      public function cm_duplicate_provider(): array {
 976          return [
 977              'valid cms without target section' => [
 978                  'targetsection' => '',
 979                  'validcms' => true,
 980                  'role' => 'admin',
 981                  'expectedexception' => false,
 982              ],
 983              'valid cms targeting an empty section' => [
 984                  'targetsection' => 'section3',
 985                  'validcms' => true,
 986                  'role' => 'admin',
 987                  'expectedexception' => false,
 988              ],
 989              'valid cms targeting a section with activities' => [
 990                  'targetsection' => 'section2',
 991                  'validcms' => true,
 992                  'role' => 'admin',
 993                  'expectedexception' => false,
 994              ],
 995              'invalid cms without target section' => [
 996                  'targetsection' => '',
 997                  'validcms' => false,
 998                  'role' => 'admin',
 999                  'expectedexception' => true,
1000              ],
1001              'invalid cms with target section' => [
1002                  'targetsection' => 'section3',
1003                  'validcms' => false,
1004                  'role' => 'admin',
1005                  'expectedexception' => true,
1006              ],
1007              'student role with target section' => [
1008                  'targetsection' => 'section3',
1009                  'validcms' => true,
1010                  'role' => 'student',
1011                  'expectedexception' => true,
1012              ],
1013              'student role without target section' => [
1014                  'targetsection' => '',
1015                  'validcms' => true,
1016                  'role' => 'student',
1017                  'expectedexception' => true,
1018              ],
1019              'unrenolled user with target section' => [
1020                  'targetsection' => 'section3',
1021                  'validcms' => true,
1022                  'role' => 'unenroled',
1023                  'expectedexception' => true,
1024              ],
1025              'unrenolled user without target section' => [
1026                  'targetsection' => '',
1027                  'validcms' => true,
1028                  'role' => 'unenroled',
1029                  'expectedexception' => true,
1030              ],
1031          ];
1032      }
1033  
1034      /**
1035       * Test for cm_delete
1036       *
1037       * @covers ::cm_delete
1038       * @dataProvider basic_role_provider
1039       * @param string $role the user role
1040       * @param bool $expectedexception if it will expect an exception.
1041       */
1042      public function test_cm_delete(
1043          string $role = 'editingteacher',
1044          bool $expectedexception = false
1045      ): void {
1046          $this->resetAfterTest();
1047          // We want modules to be deleted for good.
1048          set_config('coursebinenable', 0, 'tool_recyclebin');
1049  
1050          $info = $this->basic_state_text(
1051              'cm_delete',
1052              $role,
1053              ['cm2', 'cm3'],
1054              $expectedexception,
1055              ['remove' => 2, 'put' => 1],
1056          );
1057  
1058          $course = $info['course'];
1059          $references = $info['references'];
1060          $results = $info['results'];
1061          $courseformat = course_get_format($course->id);
1062  
1063          $this->assertArrayNotHasKey($references['cm0'], $results['remove']['cm']);
1064          $this->assertArrayNotHasKey($references['cm1'], $results['remove']['cm']);
1065          $this->assertArrayHasKey($references['cm2'], $results['remove']['cm']);
1066          $this->assertArrayHasKey($references['cm3'], $results['remove']['cm']);
1067          $this->assertArrayNotHasKey($references['cm4'], $results['remove']['cm']);
1068          $this->assertArrayNotHasKey($references['cm5'], $results['remove']['cm']);
1069  
1070          // Check the new section cm list.
1071          $newcmlist = $this->translate_references($references, ['cm4', 'cm5']);
1072          $section = $results['put']['section'][$references['section2']];
1073          $this->assertEquals($newcmlist, $section->cmlist);
1074  
1075          // Check activities are deleted.
1076          $modinfo = $courseformat->get_modinfo();
1077          $cms = $modinfo->get_cms();
1078          $this->assertArrayHasKey($references['cm0'], $cms);
1079          $this->assertArrayHasKey($references['cm1'], $cms);
1080          $this->assertArrayNotHasKey($references['cm2'], $cms);
1081          $this->assertArrayNotHasKey($references['cm3'], $cms);
1082          $this->assertArrayHasKey($references['cm4'], $cms);
1083          $this->assertArrayHasKey($references['cm5'], $cms);
1084      }
1085  
1086      /**
1087       * Test for cm_moveright
1088       *
1089       * @covers ::cm_moveright
1090       * @dataProvider basic_role_provider
1091       * @param string $role the user role
1092       * @param bool $expectedexception if it will expect an exception.
1093       */
1094      public function test_cm_moveright(
1095          string $role = 'editingteacher',
1096          bool $expectedexception = false
1097      ): void {
1098          $this->basic_state_text(
1099              'cm_moveright',
1100              $role,
1101              ['cm0', 'cm1', 'cm2', 'cm3'],
1102              $expectedexception,
1103              ['put' => 4],
1104              null,
1105              null,
1106              null,
1107              null,
1108              'indent',
1109              1
1110          );
1111      }
1112  
1113      /**
1114       * Test for cm_moveleft
1115       *
1116       * @covers ::cm_moveleft
1117       * @dataProvider basic_role_provider
1118       * @param string $role the user role
1119       * @param bool $expectedexception if it will expect an exception.
1120       */
1121      public function test_cm_moveleft(
1122          string $role = 'editingteacher',
1123          bool $expectedexception = false
1124      ): void {
1125          $this->basic_state_text(
1126              'cm_moveleft',
1127              $role,
1128              ['cm0', 'cm1', 'cm2', 'cm3'],
1129              $expectedexception,
1130              ['put' => 4],
1131              null,
1132              null,
1133              null,
1134              null,
1135              'indent',
1136              0
1137          );
1138      }
1139  
1140      /**
1141       * Test for cm_nogroups
1142       *
1143       * @covers ::cm_nogroups
1144       * @dataProvider basic_role_provider
1145       * @param string $role the user role
1146       * @param bool $expectedexception if it will expect an exception.
1147       */
1148      public function test_cm_nogroups(
1149          string $role = 'editingteacher',
1150          bool $expectedexception = false
1151      ): void {
1152          $this->basic_state_text(
1153              'cm_nogroups',
1154              $role,
1155              ['cm0', 'cm1', 'cm2', 'cm3'],
1156              $expectedexception,
1157              ['put' => 4],
1158              null,
1159              null,
1160              null,
1161              null,
1162              'groupmode',
1163              NOGROUPS
1164          );
1165      }
1166  
1167      /**
1168       * Test for cm_visiblegroups
1169       *
1170       * @covers ::cm_visiblegroups
1171       * @dataProvider basic_role_provider
1172       * @param string $role the user role
1173       * @param bool $expectedexception if it will expect an exception.
1174       */
1175      public function test_cm_visiblegroups(
1176          string $role = 'editingteacher',
1177          bool $expectedexception = false
1178      ): void {
1179          $this->basic_state_text(
1180              'cm_visiblegroups',
1181              $role,
1182              ['cm0', 'cm1', 'cm2', 'cm3'],
1183              $expectedexception,
1184              ['put' => 4],
1185              null,
1186              null,
1187              null,
1188              null,
1189              'groupmode',
1190              VISIBLEGROUPS
1191          );
1192      }
1193  
1194      /**
1195       * Test for cm_separategroups
1196       *
1197       * @covers ::cm_separategroups
1198       * @dataProvider basic_role_provider
1199       * @param string $role the user role
1200       * @param bool $expectedexception if it will expect an exception.
1201       */
1202      public function test_cm_separategroups(
1203          string $role = 'editingteacher',
1204          bool $expectedexception = false
1205      ): void {
1206          $this->basic_state_text(
1207              'cm_separategroups',
1208              $role,
1209              ['cm0', 'cm1', 'cm2', 'cm3'],
1210              $expectedexception,
1211              ['put' => 4],
1212              null,
1213              null,
1214              null,
1215              null,
1216              'groupmode',
1217              SEPARATEGROUPS
1218          );
1219      }
1220  
1221      /**
1222       * Test for section_move_after
1223       *
1224       * @covers ::section_move_after
1225       * @dataProvider section_move_after_provider
1226       * @param string[] $sectiontomove the sections to move
1227       * @param string $targetsection the target section reference
1228       * @param string[] $finalorder the final sections order
1229       * @param string[] $updatedcms the list of cms in the state updates
1230       * @param int $totalputs the total amount of put updates
1231       */
1232      public function test_section_move_after(
1233          array $sectiontomove,
1234          string $targetsection,
1235          array $finalorder,
1236          array $updatedcms,
1237          int $totalputs
1238      ): void {
1239          $this->resetAfterTest();
1240  
1241          $course = $this->create_course('topics', 8, []);
1242  
1243          $references = $this->course_references($course);
1244  
1245          // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
1246          $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
1247          $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
1248          $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
1249          $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
1250          $references["cm4"] = $this->create_activity($course->id, 'forum', 3, false);
1251          $references["cm5"] = $this->create_activity($course->id, 'wiki', 3, false);
1252  
1253          $user = $this->getDataGenerator()->create_user();
1254          $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
1255          $this->setUser($user);
1256  
1257          // Initialise stateupdates.
1258          $courseformat = course_get_format($course->id);
1259          $updates = new stateupdates($courseformat);
1260  
1261          // Execute the method.
1262          $actions = new stateactions();
1263          $actions->section_move_after(
1264              $updates,
1265              $course,
1266              $this->translate_references($references, $sectiontomove),
1267              $references[$targetsection]
1268          );
1269  
1270          // Format results in a way we can compare easily.
1271          $results = $this->summarize_updates($updates);
1272  
1273          // Validate we have all the expected entries.
1274          $this->assertEquals(0, $results['create']['count']);
1275          $this->assertEquals(0, $results['remove']['count']);
1276          // Moving a section puts:
1277          // - The course state.
1278          // - All sections state.
1279          // - The cm states related to the moved and target sections.
1280          $this->assertEquals($totalputs, $results['put']['count']);
1281  
1282          // Course state should contain the sorted list of sections (section zero + 8 sections).
1283          $finalsectionids = $this->translate_references($references, $finalorder);
1284          $coursestate = reset($results['put']['course']);
1285          $this->assertEquals($finalsectionids, $coursestate->sectionlist);
1286          // All sections should be present in the update.
1287          $this->assertCount(9, $results['put']['section']);
1288          // Only cms from the affected sections should be updated.
1289          $cmids = $this->translate_references($references, $updatedcms);
1290          $cms = $results['put']['cm'];
1291          foreach ($cmids as $cmid) {
1292              $this->assertArrayHasKey($cmid, $cms);
1293          }
1294      }
1295  
1296      /**
1297       * Provider for test_section_move_after.
1298       *
1299       * @return array the testing scenarios
1300       */
1301      public function section_move_after_provider(): array {
1302          return [
1303              'Move sections down' => [
1304                  'sectiontomove' => ['section2', 'section4'],
1305                  'targetsection' => 'section7',
1306                  'finalorder' => [
1307                      'section0',
1308                      'section1',
1309                      'section3',
1310                      'section5',
1311                      'section6',
1312                      'section7',
1313                      'section2',
1314                      'section4',
1315                      'section8',
1316                  ],
1317                  'updatedcms' => ['cm2', 'cm3'],
1318                  'totalputs' => 12,
1319              ],
1320              'Move sections up' => [
1321                  'sectiontomove' => ['section3', 'section5'],
1322                  'targetsection' => 'section1',
1323                  'finalorder' => [
1324                      'section0',
1325                      'section1',
1326                      'section3',
1327                      'section5',
1328                      'section2',
1329                      'section4',
1330                      'section6',
1331                      'section7',
1332                      'section8',
1333                  ],
1334                  'updatedcms' => ['cm0', 'cm1', 'cm4', 'cm5'],
1335                  'totalputs' => 14,
1336              ],
1337              'Move sections in the middle' => [
1338                  'sectiontomove' => ['section2', 'section5'],
1339                  'targetsection' => 'section3',
1340                  'finalorder' => [
1341                      'section0',
1342                      'section1',
1343                      'section3',
1344                      'section2',
1345                      'section5',
1346                      'section4',
1347                      'section6',
1348                      'section7',
1349                      'section8',
1350                  ],
1351                  'updatedcms' => ['cm2', 'cm3', 'cm4', 'cm5'],
1352                  'totalputs' => 14,
1353              ],
1354              'Move sections on top' => [
1355                  'sectiontomove' => ['section3', 'section5'],
1356                  'targetsection' => 'section0',
1357                  'finalorder' => [
1358                      'section0',
1359                      'section3',
1360                      'section5',
1361                      'section1',
1362                      'section2',
1363                      'section4',
1364                      'section6',
1365                      'section7',
1366                      'section8',
1367                  ],
1368                  'updatedcms' => ['cm4', 'cm5'],
1369                  'totalputs' => 12,
1370              ],
1371              'Move sections on bottom' => [
1372                  'sectiontomove' => ['section3', 'section5'],
1373                  'targetsection' => 'section8',
1374                  'finalorder' => [
1375                      'section0',
1376                      'section1',
1377                      'section2',
1378                      'section4',
1379                      'section6',
1380                      'section7',
1381                      'section8',
1382                      'section3',
1383                      'section5',
1384                  ],
1385                  'updatedcms' => ['cm4', 'cm5'],
1386                  'totalputs' => 12,
1387              ],
1388          ];
1389      }
1390  
1391      /**
1392       * Test for section_move_after capability checks.
1393       *
1394       * @covers ::section_move_after
1395       * @dataProvider basic_role_provider
1396       * @param string $role the user role
1397       * @param bool $expectedexception if it will expect an exception.
1398       */
1399      public function test_section_move_after_capabilities(
1400          string $role = 'editingteacher',
1401          bool $expectedexception = false
1402      ): void {
1403          $this->resetAfterTest();
1404          // We want modules to be deleted for good.
1405          set_config('coursebinenable', 0, 'tool_recyclebin');
1406  
1407          $info = $this->basic_state_text(
1408              'section_move_after',
1409              $role,
1410              ['section2'],
1411              $expectedexception,
1412              ['put' => 9],
1413              null,
1414              0,
1415              null,
1416              0,
1417              null,
1418              0,
1419              'section0'
1420          );
1421      }
1422  }