Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 400 and 402] [Versions 401 and 402] [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 section_move_after
1142       *
1143       * @covers ::section_move_after
1144       * @dataProvider section_move_after_provider
1145       * @param string[] $sectiontomove the sections to move
1146       * @param string $targetsection the target section reference
1147       * @param string[] $finalorder the final sections order
1148       * @param string[] $updatedcms the list of cms in the state updates
1149       * @param int $totalputs the total amount of put updates
1150       */
1151      public function test_section_move_after(
1152          array $sectiontomove,
1153          string $targetsection,
1154          array $finalorder,
1155          array $updatedcms,
1156          int $totalputs
1157      ): void {
1158          $this->resetAfterTest();
1159  
1160          $course = $this->create_course('topics', 8, []);
1161  
1162          $references = $this->course_references($course);
1163  
1164          // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
1165          $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
1166          $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
1167          $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
1168          $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
1169          $references["cm4"] = $this->create_activity($course->id, 'forum', 3, false);
1170          $references["cm5"] = $this->create_activity($course->id, 'wiki', 3, false);
1171  
1172          $user = $this->getDataGenerator()->create_user();
1173          $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
1174          $this->setUser($user);
1175  
1176          // Initialise stateupdates.
1177          $courseformat = course_get_format($course->id);
1178          $updates = new stateupdates($courseformat);
1179  
1180          // Execute the method.
1181          $actions = new stateactions();
1182          $actions->section_move_after(
1183              $updates,
1184              $course,
1185              $this->translate_references($references, $sectiontomove),
1186              $references[$targetsection]
1187          );
1188  
1189          // Format results in a way we can compare easily.
1190          $results = $this->summarize_updates($updates);
1191  
1192          // Validate we have all the expected entries.
1193          $this->assertEquals(0, $results['create']['count']);
1194          $this->assertEquals(0, $results['remove']['count']);
1195          // Moving a section puts:
1196          // - The course state.
1197          // - All sections state.
1198          // - The cm states related to the moved and target sections.
1199          $this->assertEquals($totalputs, $results['put']['count']);
1200  
1201          // Course state should contain the sorted list of sections (section zero + 8 sections).
1202          $finalsectionids = $this->translate_references($references, $finalorder);
1203          $coursestate = reset($results['put']['course']);
1204          $this->assertEquals($finalsectionids, $coursestate->sectionlist);
1205          // All sections should be present in the update.
1206          $this->assertCount(9, $results['put']['section']);
1207          // Only cms from the affected sections should be updated.
1208          $cmids = $this->translate_references($references, $updatedcms);
1209          $cms = $results['put']['cm'];
1210          foreach ($cmids as $cmid) {
1211              $this->assertArrayHasKey($cmid, $cms);
1212          }
1213      }
1214  
1215      /**
1216       * Provider for test_section_move_after.
1217       *
1218       * @return array the testing scenarios
1219       */
1220      public function section_move_after_provider(): array {
1221          return [
1222              'Move sections down' => [
1223                  'sectiontomove' => ['section2', 'section4'],
1224                  'targetsection' => 'section7',
1225                  'finalorder' => [
1226                      'section0',
1227                      'section1',
1228                      'section3',
1229                      'section5',
1230                      'section6',
1231                      'section7',
1232                      'section2',
1233                      'section4',
1234                      'section8',
1235                  ],
1236                  'updatedcms' => ['cm2', 'cm3'],
1237                  'totalputs' => 12,
1238              ],
1239              'Move sections up' => [
1240                  'sectiontomove' => ['section3', 'section5'],
1241                  'targetsection' => 'section1',
1242                  'finalorder' => [
1243                      'section0',
1244                      'section1',
1245                      'section3',
1246                      'section5',
1247                      'section2',
1248                      'section4',
1249                      'section6',
1250                      'section7',
1251                      'section8',
1252                  ],
1253                  'updatedcms' => ['cm0', 'cm1', 'cm4', 'cm5'],
1254                  'totalputs' => 14,
1255              ],
1256              'Move sections in the middle' => [
1257                  'sectiontomove' => ['section2', 'section5'],
1258                  'targetsection' => 'section3',
1259                  'finalorder' => [
1260                      'section0',
1261                      'section1',
1262                      'section3',
1263                      'section2',
1264                      'section5',
1265                      'section4',
1266                      'section6',
1267                      'section7',
1268                      'section8',
1269                  ],
1270                  'updatedcms' => ['cm2', 'cm3', 'cm4', 'cm5'],
1271                  'totalputs' => 14,
1272              ],
1273              'Move sections on top' => [
1274                  'sectiontomove' => ['section3', 'section5'],
1275                  'targetsection' => 'section0',
1276                  'finalorder' => [
1277                      'section0',
1278                      'section3',
1279                      'section5',
1280                      'section1',
1281                      'section2',
1282                      'section4',
1283                      'section6',
1284                      'section7',
1285                      'section8',
1286                  ],
1287                  'updatedcms' => ['cm4', 'cm5'],
1288                  'totalputs' => 12,
1289              ],
1290              'Move sections on bottom' => [
1291                  'sectiontomove' => ['section3', 'section5'],
1292                  'targetsection' => 'section8',
1293                  'finalorder' => [
1294                      'section0',
1295                      'section1',
1296                      'section2',
1297                      'section4',
1298                      'section6',
1299                      'section7',
1300                      'section8',
1301                      'section3',
1302                      'section5',
1303                  ],
1304                  'updatedcms' => ['cm4', 'cm5'],
1305                  'totalputs' => 12,
1306              ],
1307          ];
1308      }
1309  
1310      /**
1311       * Test for section_move_after capability checks.
1312       *
1313       * @covers ::section_move_after
1314       * @dataProvider basic_role_provider
1315       * @param string $role the user role
1316       * @param bool $expectedexception if it will expect an exception.
1317       */
1318      public function test_section_move_after_capabilities(
1319          string $role = 'editingteacher',
1320          bool $expectedexception = false
1321      ): void {
1322          $this->resetAfterTest();
1323          // We want modules to be deleted for good.
1324          set_config('coursebinenable', 0, 'tool_recyclebin');
1325  
1326          $info = $this->basic_state_text(
1327              'section_move_after',
1328              $role,
1329              ['section2'],
1330              $expectedexception,
1331              ['put' => 9],
1332              null,
1333              0,
1334              null,
1335              0,
1336              null,
1337              0,
1338              'section0'
1339          );
1340      }
1341  }