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  /**
  18   * Course related unit tests
  19   *
  20   * @package    core_course
  21   * @copyright  2014 Marina Glancy
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   * @coversDefaultClass \core_courseformat\base
  24   */
  25  class base_test extends advanced_testcase {
  26  
  27      /**
  28       * Setup to ensure that fixtures are loaded.
  29       */
  30      public static function setupBeforeClass(): void {
  31          global $CFG;
  32          require_once($CFG->dirroot . '/course/lib.php');
  33          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
  34          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
  35          require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_invalidoutput.php');
  36      }
  37  
  38      /**
  39       * Tests the save and load functionality.
  40       *
  41       * @author Jason den Dulk
  42       * @covers \core_courseformat
  43       */
  44      public function test_courseformat_saveandload() {
  45          $this->resetAfterTest();
  46  
  47          $courseformatoptiondata = (object) [
  48              "hideoddsections" => 1,
  49              'summary_editor' => [
  50                  'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
  51                  'format' => 1
  52              ]
  53          ];
  54          $generator = $this->getDataGenerator();
  55          $course1 = $generator->create_course(array('format' => 'theunittest'));
  56          $this->assertEquals('theunittest', $course1->format);
  57          course_create_sections_if_missing($course1, array(0, 1));
  58  
  59          $courseformat = course_get_format($course1);
  60          $courseformat->update_course_format_options($courseformatoptiondata);
  61  
  62          $savedcourseformatoptiondata = $courseformat->get_format_options();
  63  
  64          $this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $savedcourseformatoptiondata);
  65      }
  66  
  67      public function test_available_hook() {
  68          global $DB;
  69          $this->resetAfterTest();
  70  
  71          // Generate a course with two sections (0 and 1) and two modules. Course format is set to 'theunittest'.
  72          $generator = $this->getDataGenerator();
  73          $course1 = $generator->create_course(array('format' => 'theunittest'));
  74          $this->assertEquals('theunittest', $course1->format);
  75          course_create_sections_if_missing($course1, array(0, 1));
  76          $assign0 = $generator->create_module('assign', array('course' => $course1, 'section' => 0));
  77          $assign1 = $generator->create_module('assign', array('course' => $course1, 'section' => 1));
  78          $assign2 = $generator->create_module('assign', array('course' => $course1, 'section' => 0, 'visible' => 0));
  79  
  80          // Create a courseoverview role based on the student role.
  81          $roleattr = array('name' => 'courseoverview', 'shortname' => 'courseoverview', 'archetype' => 'student');
  82          $generator->create_role($roleattr);
  83  
  84          // Create user student, editingteacher, teacher and courseoverview.
  85          $student = $generator->create_user();
  86          $teacher = $generator->create_user();
  87          $editingteacher = $generator->create_user();
  88          $courseoverviewuser = $generator->create_user();
  89  
  90          // Enrol users into their roles.
  91          $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
  92          $generator->enrol_user($student->id, $course1->id, $roleids['student']);
  93          $generator->enrol_user($teacher->id, $course1->id, $roleids['teacher']);
  94          $generator->enrol_user($editingteacher->id, $course1->id, $roleids['editingteacher']);
  95          $generator->enrol_user($courseoverviewuser->id, $course1->id, $roleids['courseoverview']);
  96  
  97          // Remove the ignoreavailabilityrestrictions from the teacher role.
  98          role_change_permission($roleids['teacher'], context_system::instance(0),
  99                  'moodle/course:ignoreavailabilityrestrictions', CAP_PREVENT);
 100  
 101          // Allow the courseoverview role to ingore available restriction.
 102          role_change_permission($roleids['courseoverview'], context_system::instance(0),
 103                  'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW);
 104  
 105          // Make sure that initially both sections and both modules are available and visible for a student.
 106          $modinfostudent = get_fast_modinfo($course1, $student->id);
 107          $this->assertTrue($modinfostudent->get_section_info(1)->available);
 108          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 109          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 110          $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->available);
 111          $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->uservisible);
 112          $this->assertFalse($modinfostudent->get_cm($assign2->cmid)->uservisible);
 113  
 114          // Set 'hideoddsections' for the course to 1.
 115          // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher.
 116          $data = (object)array('id' => $course1->id, 'hideoddsections' => 1);
 117          course_get_format($course1)->update_course_format_options($data);
 118          $modinfostudent = get_fast_modinfo($course1, $student->id);
 119          $this->assertFalse($modinfostudent->get_section_info(1)->available);
 120          $this->assertEmpty($modinfostudent->get_section_info(1)->availableinfo);
 121          $this->assertFalse($modinfostudent->get_section_info(1)->uservisible);
 122          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 123          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 124          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available);
 125          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible);
 126          $this->assertFalse($modinfostudent->get_cm($assign2->cmid)->uservisible);
 127  
 128          $modinfoteacher = get_fast_modinfo($course1, $teacher->id);
 129          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 130          $this->assertEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 131          $this->assertFalse($modinfoteacher->get_section_info(1)->uservisible);
 132          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 133          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 134          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 135          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 136          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->available);
 137          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->uservisible);
 138  
 139          $modinfoteacher = get_fast_modinfo($course1, $editingteacher->id);
 140          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 141          $this->assertEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 142          $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible);
 143          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 144          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 145          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 146          $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 147          $this->assertTrue($modinfoteacher->get_cm($assign2->cmid)->uservisible);
 148  
 149          $modinfocourseoverview = get_fast_modinfo($course1, $courseoverviewuser->id);
 150          $this->assertFalse($modinfocourseoverview->get_section_info(1)->available);
 151          $this->assertEmpty($modinfocourseoverview->get_section_info(1)->availableinfo);
 152          $this->assertTrue($modinfocourseoverview->get_section_info(1)->uservisible);
 153          $this->assertTrue($modinfocourseoverview->get_cm($assign0->cmid)->available);
 154          $this->assertTrue($modinfocourseoverview->get_cm($assign0->cmid)->uservisible);
 155          $this->assertFalse($modinfocourseoverview->get_cm($assign1->cmid)->available);
 156          $this->assertTrue($modinfocourseoverview->get_cm($assign1->cmid)->uservisible);
 157          $this->assertFalse($modinfocourseoverview->get_cm($assign2->cmid)->uservisible);
 158  
 159          // Set 'hideoddsections' for the course to 2.
 160          // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher.
 161          // Property availableinfo will be not empty.
 162          $data = (object)array('id' => $course1->id, 'hideoddsections' => 2);
 163          course_get_format($course1)->update_course_format_options($data);
 164          $modinfostudent = get_fast_modinfo($course1, $student->id);
 165          $this->assertFalse($modinfostudent->get_section_info(1)->available);
 166          $this->assertNotEmpty($modinfostudent->get_section_info(1)->availableinfo);
 167          $this->assertFalse($modinfostudent->get_section_info(1)->uservisible);
 168          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available);
 169          $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible);
 170          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available);
 171          $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible);
 172  
 173          $modinfoteacher = get_fast_modinfo($course1, $editingteacher->id);
 174          $this->assertFalse($modinfoteacher->get_section_info(1)->available);
 175          $this->assertNotEmpty($modinfoteacher->get_section_info(1)->availableinfo);
 176          $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible);
 177          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available);
 178          $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible);
 179          $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available);
 180          $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible);
 181      }
 182  
 183      /**
 184       * Test for supports_news() with a course format plugin that doesn't define 'news_items' in default blocks.
 185       */
 186      public function test_supports_news() {
 187          $this->resetAfterTest();
 188          $format = course_get_format((object)['format' => 'testformat']);
 189          $this->assertFalse($format->supports_news());
 190      }
 191  
 192      /**
 193       * Test for supports_news() for old course format plugins that defines 'news_items' in default blocks.
 194       */
 195      public function test_supports_news_legacy() {
 196          $this->resetAfterTest();
 197          $format = course_get_format((object)['format' => 'testlegacy']);
 198          $this->assertTrue($format->supports_news());
 199      }
 200  
 201      /**
 202       * Test for get_view_url() to ensure that the url is only given for the correct cases
 203       */
 204      public function test_get_view_url() {
 205          global $CFG;
 206          $this->resetAfterTest();
 207  
 208          $linkcoursesections = $CFG->linkcoursesections;
 209  
 210          // Generate a course with two sections (0 and 1) and two modules. Course format is set to 'testformat'.
 211          // This will allow us to test the default implementation of get_view_url.
 212          $generator = $this->getDataGenerator();
 213          $course1 = $generator->create_course(array('format' => 'testformat'));
 214          course_create_sections_if_missing($course1, array(0, 1));
 215  
 216          $data = (object)['id' => $course1->id];
 217          $format = course_get_format($course1);
 218          $format->update_course_format_options($data);
 219  
 220          // In page.
 221          $CFG->linkcoursesections = 0;
 222          $this->assertNotEmpty($format->get_view_url(null));
 223          $this->assertNotEmpty($format->get_view_url(0));
 224          $this->assertNotEmpty($format->get_view_url(1));
 225          $CFG->linkcoursesections = 1;
 226          $this->assertNotEmpty($format->get_view_url(null));
 227          $this->assertNotEmpty($format->get_view_url(0));
 228          $this->assertNotEmpty($format->get_view_url(1));
 229  
 230          // Navigation.
 231          $CFG->linkcoursesections = 0;
 232          $this->assertNull($format->get_view_url(1, ['navigation' => 1]));
 233          $this->assertNull($format->get_view_url(0, ['navigation' => 1]));
 234          $CFG->linkcoursesections = 1;
 235          $this->assertNotEmpty($format->get_view_url(1, ['navigation' => 1]));
 236          $this->assertNotEmpty($format->get_view_url(0, ['navigation' => 1]));
 237  
 238          // Expand section.
 239          // The current course format $format uses the format 'testformat' which does not use sections.
 240          // Thus, the 'expanded' parameter does not do anything.
 241          $viewurl = $format->get_view_url(1);
 242          $this->assertNull($viewurl->get_param('expandsection'));
 243          $viewurl = $format->get_view_url(1, ['expanded' => 1]);
 244          $this->assertNull($viewurl->get_param('expandsection'));
 245          $viewurl = $format->get_view_url(1, ['expanded' => 0]);
 246          $this->assertNull($viewurl->get_param('expandsection'));
 247          // We now use a course format which uses sections.
 248          $course2 = $generator->create_course(['format' => 'testformatsections']);
 249          course_create_sections_if_missing($course1, [0, 2]);
 250          $formatwithsections = course_get_format($course2);
 251          $viewurl = $formatwithsections->get_view_url(2);
 252          $this->assertEquals(2, $viewurl->get_param('expandsection'));
 253          $viewurl = $formatwithsections->get_view_url(2, ['expanded' => 1]);
 254          $this->assertEquals(2, $viewurl->get_param('expandsection'));
 255          $viewurl = $formatwithsections->get_view_url(2, ['expanded' => 0]);
 256          $this->assertNull($viewurl->get_param('expandsection'));
 257      }
 258  
 259      /**
 260       * Test for get_output_classname method.
 261       *
 262       * @dataProvider get_output_classname_provider
 263       * @param string $find the class to find
 264       * @param string $result the expected result classname
 265       * @param bool $exception if the method will raise an exception
 266       */
 267      public function test_get_output_classname($find, $result, $exception) {
 268          $this->resetAfterTest();
 269  
 270          $course = $this->getDataGenerator()->create_course(['format' => 'theunittest']);
 271          $courseformat = course_get_format($course);
 272  
 273          if ($exception) {
 274              $this->expectException(coding_exception::class);
 275          }
 276  
 277          $courseclass = $courseformat->get_output_classname($find);
 278          $this->assertEquals($result, $courseclass);
 279      }
 280  
 281      /**
 282       * Data provider for test_get_output_classname.
 283       *
 284       * @return array the testing scenarios
 285       */
 286      public function get_output_classname_provider(): array {
 287          return [
 288              'overridden class' => [
 289                  'find' => 'state\\course',
 290                  'result' => 'format_theunittest\\output\\courseformat\\state\\course',
 291                  'exception' => false,
 292              ],
 293              'original class' => [
 294                  'find' => 'state\\section',
 295                  'result' => 'core_courseformat\\output\\local\\state\\section',
 296                  'exception' => false,
 297              ],
 298              'invalid overridden class' => [
 299                  'find' => 'state\\invalidoutput',
 300                  'result' => '',
 301                  'exception' => true,
 302              ],
 303          ];
 304      }
 305  
 306      /**
 307       * Test for the default delete format data behaviour.
 308       *
 309       * @covers ::get_sections_preferences
 310       */
 311      public function test_get_sections_preferences() {
 312          $this->resetAfterTest();
 313          $generator = $this->getDataGenerator();
 314          $course = $generator->create_course();
 315          $user = $generator->create_and_enrol($course, 'student');
 316  
 317          // Create fake preferences generated by the frontend js module.
 318          $data = (object)[
 319              'pref1' => [1,2],
 320              'pref2' => [1],
 321          ];
 322          set_user_preference('coursesectionspreferences_' . $course->id, json_encode($data), $user->id);
 323  
 324          $format = course_get_format($course);
 325  
 326          // Load data from user 1.
 327          $this->setUser($user);
 328          $preferences = $format->get_sections_preferences();
 329  
 330          $this->assertEquals(
 331              (object)['pref1' => true, 'pref2' => true],
 332              $preferences[1]
 333          );
 334          $this->assertEquals(
 335              (object)['pref1' => true],
 336              $preferences[2]
 337          );
 338      }
 339  
 340      /**
 341       * Test for the default delete format data behaviour.
 342       *
 343       * @covers ::set_sections_preference
 344       */
 345      public function test_set_sections_preference() {
 346          $this->resetAfterTest();
 347          $generator = $this->getDataGenerator();
 348          $course = $generator->create_course();
 349          $user = $generator->create_and_enrol($course, 'student');
 350  
 351          $format = course_get_format($course);
 352          $this->setUser($user);
 353  
 354          // Load data from user 1.
 355          $format->set_sections_preference('pref1', [1, 2]);
 356          $format->set_sections_preference('pref2', [1]);
 357          $format->set_sections_preference('pref3', []);
 358  
 359          $preferences = $format->get_sections_preferences();
 360          $this->assertEquals(
 361              (object)['pref1' => true, 'pref2' => true],
 362              $preferences[1]
 363          );
 364          $this->assertEquals(
 365              (object)['pref1' => true],
 366              $preferences[2]
 367          );
 368      }
 369  
 370      /**
 371       * Test that retrieving last section number for a course
 372       *
 373       * @covers ::get_last_section_number
 374       */
 375      public function test_get_last_section_number(): void {
 376          global $DB;
 377  
 378          $this->resetAfterTest();
 379  
 380          // Course with two additional sections.
 381          $courseone = $this->getDataGenerator()->create_course(['numsections' => 2]);
 382          $this->assertEquals(2, course_get_format($courseone)->get_last_section_number());
 383  
 384          // Course without additional sections, section zero is the "default" section that always exists.
 385          $coursetwo = $this->getDataGenerator()->create_course(['numsections' => 0]);
 386          $this->assertEquals(0, course_get_format($coursetwo)->get_last_section_number());
 387  
 388          // Course without additional sections, manually remove section zero, as "course_delete_section" prevents that. This
 389          // simulates course data integrity issues that previously triggered errors.
 390          $coursethree = $this->getDataGenerator()->create_course(['numsections' => 0]);
 391          $DB->delete_records('course_sections', ['course' => $coursethree->id, 'section' => 0]);
 392  
 393          $this->assertEquals(-1, course_get_format($coursethree)->get_last_section_number());
 394      }
 395  
 396      /**
 397       * Test for the default delete format data behaviour.
 398       *
 399       * @covers ::delete_format_data
 400       * @dataProvider delete_format_data_provider
 401       * @param bool $usehook if it should use course_delete to trigger $format->delete_format_data as a hook
 402       */
 403      public function test_delete_format_data(bool $usehook) {
 404          global $DB;
 405  
 406          $this->resetAfterTest();
 407  
 408          $generator = $this->getDataGenerator();
 409          $course = $generator->create_course();
 410          course_create_sections_if_missing($course, [0, 1]);
 411          $user = $generator->create_and_enrol($course, 'student');
 412  
 413          // Create a coursesectionspreferences_XX preference.
 414          $key = 'coursesectionspreferences_' . $course->id;
 415          $fakevalue = 'No dark sarcasm in the classroom';
 416          set_user_preference($key, $fakevalue, $user->id);
 417          $this->assertEquals(
 418              $fakevalue,
 419              $DB->get_field('user_preferences', 'value', ['name' => $key, 'userid' => $user->id])
 420          );
 421  
 422          // Create another random user preference.
 423          $key2 = 'somepreference';
 424          $fakevalue2 = "All in all it's just another brick in the wall";
 425          set_user_preference($key2, $fakevalue2, $user->id);
 426          $this->assertEquals(
 427              $fakevalue2,
 428              $DB->get_field('user_preferences', 'value', ['name' => $key2, 'userid' => $user->id])
 429          );
 430  
 431          if ($usehook) {
 432              delete_course($course, false);
 433          } else {
 434              $format = course_get_format($course);
 435              $format->delete_format_data();
 436          }
 437  
 438          // Check which the preferences exists.
 439          $this->assertFalse(
 440              $DB->record_exists('user_preferences', ['name' => $key, 'userid' => $user->id])
 441          );
 442          set_user_preference($key2, $fakevalue2, $user->id);
 443          $this->assertEquals(
 444              $fakevalue2,
 445              $DB->get_field('user_preferences', 'value', ['name' => $key2, 'userid' => $user->id])
 446          );
 447      }
 448  
 449      /**
 450       * Data provider for test_delete_format_data.
 451       *
 452       * @return array the testing scenarios
 453       */
 454      public function delete_format_data_provider(): array {
 455          return [
 456              'direct call' => [
 457                  'usehook' => false
 458              ],
 459              'use hook' => [
 460                  'usehook' => true,
 461              ]
 462          ];
 463      }
 464  
 465      /**
 466       * Test duplicate_section()
 467       * @covers ::duplicate_section
 468       */
 469      public function test_duplicate_section() {
 470          global $DB;
 471  
 472          $this->setAdminUser();
 473          $this->resetAfterTest();
 474  
 475          $generator = $this->getDataGenerator();
 476          $course = $generator->create_course();
 477          $format = course_get_format($course);
 478  
 479          $originalsection = $DB->get_record('course_sections', ['course' => $course->id, 'section' => 1], '*', MUST_EXIST);
 480          $generator->create_module('page', ['course' => $course, 'section' => $originalsection->section]);
 481          $generator->create_module('page', ['course' => $course, 'section' => $originalsection->section]);
 482          $generator->create_module('page', ['course' => $course, 'section' => $originalsection->section]);
 483  
 484          $originalmodcount = $DB->count_records('course_modules', ['course' => $course->id, 'section' => $originalsection->id]);
 485          $this->assertEquals(3, $originalmodcount);
 486  
 487          $modinfo = get_fast_modinfo($course);
 488          $sectioninfo = $modinfo->get_section_info($originalsection->section, MUST_EXIST);
 489  
 490          $newsection = $format->duplicate_section($sectioninfo);
 491  
 492          // Verify properties are the same.
 493          foreach ($originalsection as $prop => $value) {
 494              if ($prop == 'id' || $prop == 'sequence' || $prop == 'section' || $prop == 'timemodified') {
 495                  continue;
 496              }
 497              $this->assertEquals($value, $newsection->$prop);
 498          }
 499  
 500          $newmodcount = $DB->count_records('course_modules', ['course' => $course->id, 'section' => $newsection->id]);
 501          $this->assertEquals($originalmodcount, $newmodcount);
 502      }
 503  
 504      /**
 505       * Test for the default delete format data behaviour.
 506       *
 507       * @covers ::get_format_string
 508       * @dataProvider get_format_string_provider
 509       * @param string $key the string key
 510       * @param string|null $data any string data
 511       * @param array|null $expectedstring the expected string (null for exception)
 512       */
 513      public function test_get_format_string(string $key, ?string $data, ?array $expectedstring) {
 514          global $DB;
 515  
 516          $this->resetAfterTest();
 517  
 518          $generator = $this->getDataGenerator();
 519          $course = $generator->create_course(['format' => 'topics']);
 520  
 521          if ($expectedstring) {
 522              $expected = get_string($expectedstring[0], $expectedstring[1], $expectedstring[2]);
 523          } else {
 524              $this->expectException(\coding_exception::class);
 525          }
 526          $format = course_get_format($course);
 527          $result = $format->get_format_string($key, $data);
 528          $this->assertEquals($expected, $result);
 529      }
 530  
 531      /**
 532       * Data provider for test_get_format_string.
 533       *
 534       * @return array the testing scenarios
 535       */
 536      public function get_format_string_provider(): array {
 537          return [
 538              'Existing in format lang' => [
 539                  'key' => 'sectionsdelete',
 540                  'data' => null,
 541                  'expectedstring' => ['sectionsdelete', 'format_topics', null],
 542              ],
 543              'Not existing in format lang' => [
 544                  'key' => 'bulkedit',
 545                  'data' => null,
 546                  'expectedstring' => ['bulkedit', 'core_courseformat', null],
 547              ],
 548              'Existing in format lang with data' => [
 549                  'key' => 'selectsection',
 550                  'data' => 'Example',
 551                  'expectedstring' => ['selectsection', 'format_topics', 'Example'],
 552              ],
 553              'Not existing in format lang with data' => [
 554                  'key' => 'bulkselection',
 555                  'data' => 'X',
 556                  'expectedstring' => ['bulkselection', 'core_courseformat', 'X'],
 557              ],
 558              'Non existing string' => [
 559                  'key' => '%&non_existing_string_in_lang_files$%@#',
 560                  'data' => null,
 561                  'expectedstring' => null,
 562              ],
 563          ];
 564      }
 565  
 566      /**
 567       * Test for the move_section_after method.
 568       *
 569       * @covers ::move_section_after
 570       * @dataProvider move_section_after_provider
 571       * @param string $movesection the reference of the section to move
 572       * @param string $destination the reference of the destination section
 573       * @param string[] $order the references of the final section order
 574       */
 575      public function test_move_section_after(string $movesection, string $destination, array $order) {
 576          global $DB;
 577  
 578          $this->resetAfterTest();
 579  
 580          $generator = $this->getDataGenerator();
 581          $course = $generator->create_course();
 582          course_create_sections_if_missing($course, [0, 1, 2, 3, 4, 5]);
 583  
 584          $format = course_get_format($course);
 585          $modinfo = $format->get_modinfo();
 586          $sectionsinfo = $modinfo->get_section_info_all();
 587  
 588          $references = [];
 589          foreach ($sectionsinfo as $section) {
 590              $references["section{$section->section}"] = $section;
 591          }
 592  
 593          $result = $format->move_section_after(
 594              $references[$movesection],
 595              $references[$destination]
 596          );
 597          $this->assertEquals(true, $result);
 598          // Check the updated course section list.
 599          $modinfo = $format->get_modinfo();
 600          $sectionsinfo = $modinfo->get_section_info_all();
 601          $this->assertCount(count($order), $sectionsinfo);
 602          foreach ($sectionsinfo as $key => $section) {
 603              $sectionreference = $order[$key];
 604              $oldinfo = $references[$sectionreference];
 605              $this->assertEquals($oldinfo->id, $section->id);
 606          }
 607      }
 608  
 609      /**
 610       * Data provider for test_move_section_after.
 611       *
 612       * @return array the testing scenarios
 613       */
 614      public function move_section_after_provider(): array {
 615          return [
 616              'Move top' => [
 617                  'movesection' => 'section3',
 618                  'destination' => 'section0',
 619                  'order' => [
 620                      'section0',
 621                      'section3',
 622                      'section1',
 623                      'section2',
 624                      'section4',
 625                      'section5',
 626                  ],
 627              ],
 628              'Move up' => [
 629                  'movesection' => 'section3',
 630                  'destination' => 'section1',
 631                  'order' => [
 632                      'section0',
 633                      'section1',
 634                      'section3',
 635                      'section2',
 636                      'section4',
 637                      'section5',
 638                  ],
 639              ],
 640              'Do not move' => [
 641                  'movesection' => 'section3',
 642                  'destination' => 'section2',
 643                  'order' => [
 644                      'section0',
 645                      'section1',
 646                      'section2',
 647                      'section3',
 648                      'section4',
 649                      'section5',
 650                  ],
 651              ],
 652              'Same position' => [
 653                  'movesection' => 'section3',
 654                  'destination' => 'section3',
 655                  'order' => [
 656                      'section0',
 657                      'section1',
 658                      'section2',
 659                      'section3',
 660                      'section4',
 661                      'section5',
 662                  ],
 663              ],
 664              'Move down' => [
 665                  'movesection' => 'section3',
 666                  'destination' => 'section4',
 667                  'order' => [
 668                      'section0',
 669                      'section1',
 670                      'section2',
 671                      'section4',
 672                      'section3',
 673                      'section5',
 674                  ],
 675              ],
 676              'Move bottom' => [
 677                  'movesection' => 'section3',
 678                  'destination' => 'section5',
 679                  'order' => [
 680                      'section0',
 681                      'section1',
 682                      'section2',
 683                      'section4',
 684                      'section5',
 685                      'section3',
 686                  ],
 687              ],
 688          ];
 689      }
 690  
 691      /**
 692       * Test for the get_non_ajax_cm_action_url method.
 693       *
 694       * @covers ::get_non_ajax_cm_action_url
 695       * @dataProvider get_non_ajax_cm_action_url_provider
 696       * @param string $action the ajax action name
 697       * @param string $expectedparam the expected param to check
 698       * @param string $exception if an exception is expected
 699       */
 700      public function test_get_non_ajax_cm_action_url(string $action, string $expectedparam, bool $exception) {
 701          global $DB;
 702  
 703          $this->resetAfterTest();
 704  
 705          $generator = $this->getDataGenerator();
 706          $course = $generator->create_course();
 707          $assign0 = $generator->create_module('assign', array('course' => $course, 'section' => 0));
 708  
 709          $format = course_get_format($course);
 710          $modinfo = $format->get_modinfo();
 711          $cminfo = $modinfo->get_cm($assign0->cmid);
 712  
 713          if ($exception) {
 714              $this->expectException(\coding_exception::class);
 715          }
 716          $result = $format->get_non_ajax_cm_action_url($action, $cminfo);
 717          $this->assertEquals($assign0->cmid, $result->param($expectedparam));
 718      }
 719  
 720      /**
 721       * Data provider for test_get_non_ajax_cm_action_url.
 722       *
 723       * @return array the testing scenarios
 724       */
 725      public function get_non_ajax_cm_action_url_provider(): array {
 726          return [
 727              'duplicate' => [
 728                  'action' => 'cmDuplicate',
 729                  'expectedparam' => 'duplicate',
 730                  'exception' => false,
 731              ],
 732              'hide' => [
 733                  'action' => 'cmHide',
 734                  'expectedparam' => 'hide',
 735                  'exception' => false,
 736              ],
 737              'show' => [
 738                  'action' => 'cmShow',
 739                  'expectedparam' => 'show',
 740                  'exception' => false,
 741              ],
 742              'stealth' => [
 743                  'action' => 'cmStealth',
 744                  'expectedparam' => 'stealth',
 745                  'exception' => false,
 746              ],
 747              'delete' => [
 748                  'action' => 'cmDelete',
 749                  'expectedparam' => 'delete',
 750                  'exception' => false,
 751              ],
 752              'non-existent' => [
 753                  'action' => 'nonExistent',
 754                  'expectedparam' => '',
 755                  'exception' => true,
 756              ],
 757          ];
 758      }
 759  }
 760  
 761  /**
 762   * Class format_testformat.
 763   *
 764   * A test class that simulates a course format that doesn't define 'news_items' in default blocks.
 765   *
 766   * @copyright 2016 Jun Pataleta <jun@moodle.com>
 767   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 768   */
 769  class format_testformat extends core_courseformat\base {
 770      /**
 771       * Returns the list of blocks to be automatically added for the newly created course.
 772       *
 773       * @return array
 774       */
 775      public function get_default_blocks() {
 776          return [
 777              BLOCK_POS_RIGHT => [],
 778              BLOCK_POS_LEFT => []
 779          ];
 780      }
 781  }
 782  
 783  /**
 784   * Class format_testformatsections.
 785   *
 786   * A test class that simulates a course format with sections.
 787   *
 788   * @package   core_courseformat
 789   * @copyright 2023 ISB Bayern
 790   * @author    Philipp Memmel
 791   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 792   */
 793  class format_testformatsections extends core_courseformat\base {
 794      /**
 795       * Returns if this course format uses sections.
 796       *
 797       * @return true
 798       */
 799      public function uses_sections() {
 800          return true;
 801      }
 802  }
 803  
 804  /**
 805   * Class format_testlegacy.
 806   *
 807   * A test class that simulates old course formats that define 'news_items' in default blocks.
 808   *
 809   * @copyright 2016 Jun Pataleta <jun@moodle.com>
 810   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 811   */
 812  class format_testlegacy extends core_courseformat\base {
 813      /**
 814       * Returns the list of blocks to be automatically added for the newly created course.
 815       *
 816       * @return array
 817       */
 818      public function get_default_blocks() {
 819          return [
 820              BLOCK_POS_RIGHT => ['news_items'],
 821              BLOCK_POS_LEFT => []
 822          ];
 823      }
 824  }