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.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace core_courseformat;

use course_modinfo;
use moodle_exception;
use stdClass;

/**
 * Tests for the stateactions class.
 *
 * @package    core_courseformat
 * @category   test
 * @copyright  2021 Sara Arjona (sara@moodle.com)
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @coversDefaultClass \core_courseformat\stateactions
 */
class stateactions_test extends \advanced_testcase {
    /**
     * Helper method to create an activity into a section and add it to the $sections and $activities arrays.
     *
     * @param int $courseid Course identifier where the activity will be added.
     * @param string $type Activity type ('forum', 'assign', ...).
     * @param int $section Section number where the activity will be added.
     * @param bool $visible Whether the activity will be visible or not.
     * @return int the activity cm id
     */
    private function create_activity(
        int $courseid,
        string $type,
        int $section,
        bool $visible = true
    ): int {

        $activity = $this->getDataGenerator()->create_module(
            $type,
            ['course' => $courseid],
            [
                'section' => $section,
                'visible' => $visible
            ]
        );
        return $activity->cmid;
    }

    /**
     * Helper to create a course and generate a section list.
     *
     * @param string $format the course format
     * @param int $sections the number of sections
     * @param int[] $hiddensections the section numbers to hide
     * @return stdClass the course object
     */
    private function create_course(string $format, int $sections, array $hiddensections): stdClass {
        global $DB;

        $course = $this->getDataGenerator()->create_course(['numsections' => $sections, 'format' => $format]);
        foreach ($hiddensections as $section) {
            set_section_visible($course->id, $section, 0);
        }

        return $course;
    }

    /**
     * Return an array if the course references.
     *
     * This method is used to create alias to sections and other stuff in the dataProviders.
     *
     * @param stdClass $course the course object
     * @return int[] a relation betwee all references and its element id
     */
    private function course_references(stdClass $course): array {
        global $DB;

        $references = [];

        $sectionrecords = $DB->get_records('course_sections', ['course' => $course->id]);
        foreach ($sectionrecords as $id => $section) {
            $references["section{$section->section}"] = $section->id;
        }
        $references['course'] = $course->id;
        $references['invalidsection'] = -1;
        $references['invalidcm'] = -1;

        return $references;
    }

    /**
     * Translate a references array into current ids.
     *
     * @param string[] $references the references list
     * @param string[] $values the values to translate
     * @return int[] the list of ids
     */
    private function translate_references(array $references, array $values): array {
        $result = [];
        foreach ($values as $value) {
            $result[] = $references[$value];
        }
        return $result;
    }

    /**
     * Generate a sorted and summarized list of an state updates message.
     *
     * It is important to note that the order in the update messages are not important in a real scenario
     * because each message affects a specific part of the course state. However, for the PHPUnit test
     * have them sorted and classified simplifies the asserts.
     *
     * @param stateupdates $updateobj the state updates object
     * @return array of all data updates.
     */
    private function summarize_updates(stateupdates $updateobj): array {
        // Check state returned after executing given action.
        $updatelist = $updateobj->jsonSerialize();

        // Initial summary structure.
        $result = [
            'create' => [
                'course' => [],
                'section' => [],
                'cm' => [],
                'count' => 0,
            ],
            'put' => [
                'course' => [],
                'section' => [],
                'cm' => [],
                'count' => 0,
            ],
            'remove' => [
                'course' => [],
                'section' => [],
                'cm' => [],
                'count' => 0,
            ],
        ];
        foreach ($updatelist as $update) {
            if (!isset($result[$update->action])) {
                $result[$update->action] = [
                    'course' => [],
                    'section' => [],
                    'cm' => [],
                    'count' => 0,
                ];
            }
            $elementid = $update->fields->id ?? 0;
            $result[$update->action][$update->name][$elementid] = $update->fields;
            $result[$update->action]['count']++;
        }
        return $result;
    }

    /**
     * Enrol, set and create the test user depending on the role name.
     *
     * @param stdClass $course the course data
     * @param string $rolename the testing role name
     */
    private function set_test_user_by_role(stdClass $course, string $rolename) {
        if ($rolename == 'admin') {
            $this->setAdminUser();
        } else {
            $user = $this->getDataGenerator()->create_user();
            if ($rolename != 'unenroled') {
                $this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
            }
            $this->setUser($user);
        }
    }

    /**
     * Test the behaviour course_state.
     *
     * @dataProvider get_state_provider
     * @covers ::course_state
     * @covers ::section_state
     * @covers ::cm_state
     *
     * @param string $format The course will be created with this course format.
     * @param string $role The role of the user that will execute the method.
     * @param string $method the method to call
     * @param array $params the ids, targetsection and targetcm to use as params
     * @param array $expectedresults List of the course module names expected after calling the method.
     * @param bool $expectedexception If this call will raise an exception.

     */
    public function test_get_state(
        string $format,
        string $role,
        string $method,
        array $params,
        array $expectedresults,
        bool $expectedexception = false
    ): void {

        $this->resetAfterTest();

        // Create a course with 3 sections, 1 of them hidden.
        $course = $this->create_course($format, 3, [2]);

        $references = $this->course_references($course);

        // Create and enrol user using given role.
        $this->set_test_user_by_role($course, $role);

        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
        $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
        $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
        $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);

        if ($expectedexception) {
            $this->expectException(moodle_exception::class);
        }

        // Initialise stateupdates.
        $courseformat = course_get_format($course->id);
        $updates = new stateupdates($courseformat);

        // Execute given method.
        $actions = new stateactions();
        $actions->$method(
            $updates,
            $course,
            $this->translate_references($references, $params['ids']),
            $references[$params['targetsectionid']] ?? null,
            $references[$params['targetcmid']] ?? null
        );

        // Format results in a way we can compare easily.
        $results = $this->summarize_updates($updates);

        // The state actions does not use create or remove actions because they are designed
        // to refresh parts of the state.
        $this->assertEquals(0, $results['create']['count']);
        $this->assertEquals(0, $results['remove']['count']);

        // Validate we have all the expected entries.
        $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']);
        $this->assertEquals($expectedtotal, $results['put']['count']);

        // Validate course, section and cm.
        foreach ($expectedresults as $name => $referencekeys) {
            foreach ($referencekeys as $referencekey) {
                $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]);
            }
        }
    }

    /**
     * Data provider for data request creation tests.
     *
     * @return array the testing scenarios
     */
    public function get_state_provider(): array {
        return array_merge(
            $this->course_state_provider('weeks'),
            $this->course_state_provider('topics'),
            $this->course_state_provider('social'),
            $this->section_state_provider('weeks', 'admin'),
            $this->section_state_provider('weeks', 'editingteacher'),
            $this->section_state_provider('weeks', 'student'),
            $this->section_state_provider('topics', 'admin'),
            $this->section_state_provider('topics', 'editingteacher'),
            $this->section_state_provider('topics', 'student'),
            $this->section_state_provider('social', 'admin'),
            $this->section_state_provider('social', 'editingteacher'),
            $this->section_state_provider('social', 'student'),
            $this->cm_state_provider('weeks', 'admin'),
            $this->cm_state_provider('weeks', 'editingteacher'),
            $this->cm_state_provider('weeks', 'student'),
            $this->cm_state_provider('topics', 'admin'),
            $this->cm_state_provider('topics', 'editingteacher'),
            $this->cm_state_provider('topics', 'student'),
            $this->cm_state_provider('social', 'admin'),
            $this->cm_state_provider('social', 'editingteacher'),
            $this->cm_state_provider('social', 'student'),
        );
    }

    /**
     * Course state data provider.
     *
     * @param string $format the course format
     * @return array the testing scenarios
     */
    public function course_state_provider(string $format): array {
        $expectedexception = ($format === 'social');
        return [
            // Tests for course_state.
            "admin $format course_state" => [
                'format' => $format,
                'role' => 'admin',
                'method' => 'course_state',
                'params' => [
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => ['course'],
                    'section' => ['section0', 'section1', 'section2', 'section3'],
                    'cm' => ['cm0', 'cm1', 'cm2', 'cm3'],
                ],
                'expectedexception' => $expectedexception,
            ],
            "editingteacher $format course_state" => [
                'format' => $format,
                'role' => 'editingteacher',
                'method' => 'course_state',
                'params' => [
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => ['course'],
                    'section' => ['section0', 'section1', 'section2', 'section3'],
                    'cm' => ['cm0', 'cm1', 'cm2', 'cm3'],
                ],
                'expectedexception' => $expectedexception,
            ],
            "student $format course_state" => [
                'format' => $format,
                'role' => 'student',
                'method' => 'course_state',
                'params' => [
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => ['course'],
                    'section' => ['section0', 'section1', 'section3'],
                    'cm' => ['cm0'],
                ],
                'expectedexception' => $expectedexception,
            ],
        ];
    }

    /**
     * Section state data provider.
     *
     * @param string $format the course format
     * @param string $role the user role
     * @return array the testing scenarios
     */
    public function section_state_provider(string $format, string $role): array {

        // Social format will raise an exception and debug messages because it does not
        // use sections and it does not provide a renderer.
        $expectedexception = ($format === 'social');

        // All sections and cms that the user can access to.
        $usersections = ['section0', 'section1', 'section2', 'section3'];
        $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
        if ($role == 'student') {
            $usersections = ['section0', 'section1', 'section3'];
            $usercms = ['cm0'];
        }

        return [
            "$role $format section_state no section" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [],
                'expectedexception' => true,
            ],
            "$role $format section_state section 0" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section0'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section0'], $usersections),
                    'cm' => [],
                ],
                'expectedexception' => $expectedexception,
            ],
            "$role $format section_state visible section" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section1'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1'], $usersections),
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
                ],
                'expectedexception' => $expectedexception,
            ],
            "$role $format section_state hidden section" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section2'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section2'], $usersections),
                    'cm' => array_intersect(['cm2', 'cm3'], $usercms),
                ],
                'expectedexception' => $expectedexception,
            ],
            "$role $format section_state several sections" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section1', 'section3'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1', 'section3'], $usersections),
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
                ],
                'expectedexception' => $expectedexception,
            ],
            "$role $format section_state invalid section" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['invalidsection'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [],
                'expectedexception' => true,
            ],
            "$role $format section_state using target section" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section1'], 'targetsectionid' => 'section3', 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1', 'section3'], $usersections),
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
                ],
                'expectedexception' => $expectedexception,
            ],
            "$role $format section_state using target targetcmid" => [
                'format' => $format,
                'role' => $role,
                'method' => 'section_state',
                'params' => [
                    'ids' => ['section3'], 'targetsectionid' => null, 'targetcmid' => 'cm1'
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section3'], $usersections),
                    'cm' => array_intersect(['cm1'], $usercms),
                ],
                'expectedexception' => $expectedexception,
            ],
        ];
    }

    /**
     * Course module state data provider.
     *
     * @param string $format the course format
     * @param string $role the user role
     * @return array the testing scenarios
     */
    public function cm_state_provider(string $format, string $role): array {

        // All sections and cms that the user can access to.
        $usersections = ['section0', 'section1', 'section2', 'section3'];
        $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
        if ($role == 'student') {
            $usersections = ['section0', 'section1', 'section3'];
            $usercms = ['cm0'];
        }

        return [
            "$role $format cm_state no cms" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [],
                'expectedexception' => true,
            ],
            "$role $format cm_state visible cm" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1'], $usersections),
                    'cm' => array_intersect(['cm0'], $usercms),
                ],
                'expectedexception' => false,
            ],
            "$role $format cm_state hidden cm" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['cm1'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1'], $usersections),
                    'cm' => array_intersect(['cm1'], $usercms),
                ],
                'expectedexception' => false,
            ],
            "$role $format cm_state several cm" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['cm0', 'cm2'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1', 'section2'], $usersections),
                    'cm' => array_intersect(['cm0', 'cm2'], $usercms),
                ],
                'expectedexception' => false,
            ],
            "$role $format cm_state using targetsection" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['cm0'], 'targetsectionid' => 'section2', 'targetcmid' => null
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1', 'section2'], $usersections),
                    'cm' => array_intersect(['cm0'], $usercms),
                ],
                'expectedexception' => ($format === 'social'),
            ],
            "$role $format cm_state using targetcm" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => 'cm3'
                ],
                'expectedresults' => [
                    'course' => [],
                    'section' => array_intersect(['section1', 'section2'], $usersections),
                    'cm' => array_intersect(['cm0', 'cm3'], $usercms),
                ],
                'expectedexception' => false,
            ],
            "$role $format cm_state using an invalid cm" => [
                'format' => $format,
                'role' => $role,
                'method' => 'cm_state',
                'params' => [
                    'ids' => ['invalidcm'], 'targetsectionid' => null, 'targetcmid' => null
                ],
                'expectedresults' => [],
                'expectedexception' => true,
            ],
        ];
    }

    /**
     * Internal method for testing a specific state action.
     *
     * @param string $method the method to test
     * @param string $role the user role
     * @param string[] $idrefs the sections or cms id references to be used as method params
     * @param bool $expectedexception whether the call should throw an exception
     * @param int[] $expectedtotal the expected total number of state indexed by put, remove and create
     * @param string|null $coursefield the course field to check
     * @param int|string|null $coursevalue the section field value
     * @param string|null $sectionfield the section field to check
     * @param int|string|null $sectionvalue the section field value
     * @param string|null $cmfield the cm field to check
     * @param int|string|null $cmvalue the cm field value
     * @param string|null $targetsection optional target section reference
     * @param string|null $targetcm optional target cm reference
     * @return array an array of elements to do extra validations (course, references, results)
     */
    protected function basic_state_text(
        string $method = 'section_hide',
        string $role = 'editingteacher',
        array $idrefs = [],
        bool $expectedexception = false,
        array $expectedtotals = [],
        ?string $coursefield = null,
        $coursevalue = 0,
        ?string $sectionfield = null,
        $sectionvalue = 0,
        ?string $cmfield = null,
        $cmvalue = 0,
        ?string $targetsection = null,
        ?string $targetcm = null
    ): array {
        $this->resetAfterTest();

        // Create a course with 3 sections, 1 of them hidden.
        $course = $this->create_course('topics', 3, [2]);

        $references = $this->course_references($course);

        $user = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
        $this->setUser($user);

        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
        $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
        $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
        $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
        $references["cm4"] = $this->create_activity($course->id, 'forum', 2, false);
        $references["cm5"] = $this->create_activity($course->id, 'wiki', 2, false);

        if ($expectedexception) {
            $this->expectException(moodle_exception::class);
        }

        // Initialise stateupdates.
        $courseformat = course_get_format($course->id);
        $updates = new stateupdates($courseformat);

        // Execute the method.
        $actions = new stateactions();
        $actions->$method(
            $updates,
            $course,
            $this->translate_references($references, $idrefs),
            ($targetsection) ? $references[$targetsection] : null,
            ($targetcm) ? $references[$targetcm] : null,
        );

        // Format results in a way we can compare easily.
        $results = $this->summarize_updates($updates);

        // Validate we have all the expected entries.
        $this->assertEquals($expectedtotals['create'] ?? 0, $results['create']['count']);
        $this->assertEquals($expectedtotals['remove'] ?? 0, $results['remove']['count']);
        $this->assertEquals($expectedtotals['put'] ?? 0, $results['put']['count']);

        // Validate course, section and cm.
        if (!empty($coursefield)) {
            foreach ($results['put']['course'] as $courseid) {
                $this->assertEquals($coursevalue, $results['put']['course'][$courseid][$coursefield]);
            }
        }
        if (!empty($sectionfield)) {
            foreach ($results['put']['section'] as $section) {
                $this->assertEquals($sectionvalue, $section->$sectionfield);
            }
        }
        if (!empty($cmfield)) {
            foreach ($results['put']['cm'] as $cm) {
                $this->assertEquals($cmvalue, $cm->$cmfield);
            }
        }
        return [
            'course' => $course,
            'references' => $references,
            'results' => $results,
        ];
    }

    /**
     * Test for section_hide
     *
     * @covers ::section_hide
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_section_hide(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'section_hide',
            $role,
            ['section1', 'section2', 'section3'],
            $expectedexception,
            ['put' => 9],
            null,
            null,
            'visible',
            0,
            null,
            null
        );
    }

    /**
     * Test for section_hide
     *
     * @covers ::section_show
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_section_show(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'section_show',
            $role,
            ['section1', 'section2', 'section3'],
            $expectedexception,
            ['put' => 9],
            null,
            null,
            'visible',
            1,
            null,
            null
        );
    }

    /**
     * Test for cm_show
     *
     * @covers ::cm_show
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_show(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'cm_show',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'visible',
            1
        );
    }

    /**
     * Test for cm_hide
     *
     * @covers ::cm_hide
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_hide(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'cm_hide',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'visible',
            0
        );
    }

    /**
     * Test for cm_stealth
     *
     * @covers ::cm_stealth
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_stealth(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        set_config('allowstealth', 1);
        $this->basic_state_text(
            'cm_stealth',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'stealth',
            1
        );
        // Disable stealth.
        set_config('allowstealth', 0);
        // When stealth are disabled the validation is a but more complex because they depends
        // also on the section visibility (legacy stealth).
        $this->basic_state_text(
            'cm_stealth',
            $role,
            ['cm0', 'cm1'],
            $expectedexception,
            ['put' => 2],
            null,
            null,
            null,
            null,
            'stealth',
            0
        );
        $this->basic_state_text(
            'cm_stealth',
            $role,
            ['cm2', 'cm3'],
            $expectedexception,
            ['put' => 2],
            null,
            null,
            null,
            null,
            'stealth',
            1
        );
    }

    /**
     * Data provider for basic role tests.
     *
     * @return array the testing scenarios
     */
    public function basic_role_provider() {
        return [
            'editingteacher' => [
                'role' => 'editingteacher',
                'expectedexception' => false,
            ],
            'teacher' => [
                'role' => 'teacher',
                'expectedexception' => true,
            ],
            'student' => [
                'role' => 'student',
                'expectedexception' => true,
            ],
            'guest' => [
                'role' => 'guest',
                'expectedexception' => true,
            ],
        ];
    }

    /**
     * Duplicate course module method.
     *
     * @covers ::cm_duplicate
     * @dataProvider cm_duplicate_provider
     * @param string $targetsection the target section (empty for none)
     * @param bool $validcms if uses valid cms
     * @param string $role the current user role name
     * @param bool $expectedexception if the test will raise an exception
     */
    public function test_cm_duplicate(
        string $targetsection = '',
        bool $validcms = true,
        string $role = 'admin',
        bool $expectedexception = false
    ) {
        $this->resetAfterTest();

        // Create a course with 3 sections.
        $course = $this->create_course('topics', 3, []);

        $references = $this->course_references($course);

        // Create and enrol user using given role.
        $this->set_test_user_by_role($course, $role);

        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
        $references["cm1"] = $this->create_activity($course->id, 'page', 2, false);

        if ($expectedexception) {
            $this->expectException(moodle_exception::class);
        }

        // Initialise stateupdates.
        $courseformat = course_get_format($course->id);
        $updates = new stateupdates($courseformat);

        // Execute method.
        $targetsectionid = (!empty($targetsection)) ? $references[$targetsection] : null;
        $cmrefs = ($validcms) ? ['cm0', 'cm1'] : ['invalidcm'];
        $actions = new stateactions();
        $actions->cm_duplicate(
            $updates,
            $course,
            $this->translate_references($references, $cmrefs),
            $targetsectionid,
        );

        // Check the new elements in the course structure.
        $originalsections = [
            'assign' => $references['section1'],
            'page' => $references['section2'],
        ];
        $modinfo = course_modinfo::instance($course);
        $cms = $modinfo->get_cms();
        $i = 0;
        foreach ($cms as $cmid => $cminfo) {
            if ($cmid == $references['cm0'] || $cmid == $references['cm1']) {
                continue;
            }
            $references["newcm$i"] = $cmid;
            if ($targetsectionid) {
                $this->assertEquals($targetsectionid, $cminfo->section);
            } else {
                $this->assertEquals($originalsections[$cminfo->modname], $cminfo->section);
            }
            $i++;
        }

        // Check the resulting updates.
        $results = $this->summarize_updates($updates);

        if ($targetsectionid) {
            $this->assertArrayHasKey($references[$targetsection], $results['put']['section']);
        } else {
            $this->assertArrayHasKey($references['section1'], $results['put']['section']);
            $this->assertArrayHasKey($references['section2'], $results['put']['section']);
        }
        $countcms = ($targetsection == 'section3' || $targetsection === '') ? 2 : 3;
        $this->assertCount($countcms, $results['put']['cm']);
        $this->assertArrayHasKey($references['newcm0'], $results['put']['cm']);
        $this->assertArrayHasKey($references['newcm1'], $results['put']['cm']);
    }

    /**
     * Duplicate course module data provider.
     *
     * @return array the testing scenarios
     */
    public function cm_duplicate_provider(): array {
        return [
            'valid cms without target section' => [
                'targetsection' => '',
                'validcms' => true,
                'role' => 'admin',
                'expectedexception' => false,
            ],
            'valid cms targeting an empty section' => [
                'targetsection' => 'section3',
                'validcms' => true,
                'role' => 'admin',
                'expectedexception' => false,
            ],
            'valid cms targeting a section with activities' => [
                'targetsection' => 'section2',
                'validcms' => true,
                'role' => 'admin',
                'expectedexception' => false,
            ],
            'invalid cms without target section' => [
                'targetsection' => '',
                'validcms' => false,
                'role' => 'admin',
                'expectedexception' => true,
            ],
            'invalid cms with target section' => [
                'targetsection' => 'section3',
                'validcms' => false,
                'role' => 'admin',
                'expectedexception' => true,
            ],
            'student role with target section' => [
                'targetsection' => 'section3',
                'validcms' => true,
                'role' => 'student',
                'expectedexception' => true,
            ],
            'student role without target section' => [
                'targetsection' => '',
                'validcms' => true,
                'role' => 'student',
                'expectedexception' => true,
            ],
            'unrenolled user with target section' => [
                'targetsection' => 'section3',
                'validcms' => true,
                'role' => 'unenroled',
                'expectedexception' => true,
            ],
            'unrenolled user without target section' => [
                'targetsection' => '',
                'validcms' => true,
                'role' => 'unenroled',
                'expectedexception' => true,
            ],
        ];
    }

    /**
     * Test for cm_delete
     *
     * @covers ::cm_delete
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_delete(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->resetAfterTest();
        // We want modules to be deleted for good.
        set_config('coursebinenable', 0, 'tool_recyclebin');

        $info = $this->basic_state_text(
            'cm_delete',
            $role,
            ['cm2', 'cm3'],
            $expectedexception,
            ['remove' => 2, 'put' => 1],
        );

        $course = $info['course'];
        $references = $info['references'];
        $results = $info['results'];
        $courseformat = course_get_format($course->id);

        $this->assertArrayNotHasKey($references['cm0'], $results['remove']['cm']);
        $this->assertArrayNotHasKey($references['cm1'], $results['remove']['cm']);
        $this->assertArrayHasKey($references['cm2'], $results['remove']['cm']);
        $this->assertArrayHasKey($references['cm3'], $results['remove']['cm']);
        $this->assertArrayNotHasKey($references['cm4'], $results['remove']['cm']);
        $this->assertArrayNotHasKey($references['cm5'], $results['remove']['cm']);

        // Check the new section cm list.
        $newcmlist = $this->translate_references($references, ['cm4', 'cm5']);
        $section = $results['put']['section'][$references['section2']];
        $this->assertEquals($newcmlist, $section->cmlist);

        // Check activities are deleted.
        $modinfo = $courseformat->get_modinfo();
        $cms = $modinfo->get_cms();
        $this->assertArrayHasKey($references['cm0'], $cms);
        $this->assertArrayHasKey($references['cm1'], $cms);
        $this->assertArrayNotHasKey($references['cm2'], $cms);
        $this->assertArrayNotHasKey($references['cm3'], $cms);
        $this->assertArrayHasKey($references['cm4'], $cms);
        $this->assertArrayHasKey($references['cm5'], $cms);
    }

    /**
     * Test for cm_moveright
     *
     * @covers ::cm_moveright
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_moveright(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'cm_moveright',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'indent',
            1
        );
    }

    /**
     * Test for cm_moveleft
     *
     * @covers ::cm_moveleft
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_moveleft(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'cm_moveleft',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'indent',
            0
        );
    }

    /**
> * Test for cm_nogroups * Test for section_move_after > * * > * @covers ::cm_nogroups * @covers ::section_move_after > * @dataProvider basic_role_provider * @dataProvider section_move_after_provider > * @param string $role the user role * @param string[] $sectiontomove the sections to move > * @param bool $expectedexception if it will expect an exception. * @param string $targetsection the target section reference > */ * @param string[] $finalorder the final sections order > public function test_cm_nogroups( * @param string[] $updatedcms the list of cms in the state updates > string $role = 'editingteacher', * @param int $totalputs the total amount of put updates > bool $expectedexception = false */ > ): void { public function test_section_move_after( > $this->basic_state_text( array $sectiontomove, > 'cm_nogroups', string $targetsection, > $role, array $finalorder, > ['cm0', 'cm1', 'cm2', 'cm3'], array $updatedcms, > $expectedexception, int $totalputs > ['put' => 4], ): void { > null, $this->resetAfterTest(); > null, > null, $course = $this->create_course('topics', 8, []); > null, > 'groupmode', $references = $this->course_references($course); > NOGROUPS > ); // Add some activities to the course. One visible and one hidden in both sections 1 and 2. > } $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true); > $references["cm1"] = $this->create_activity($course->id, 'book', 1, false); > /** $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true); > * Test for cm_visiblegroups $references["cm3"] = $this->create_activity($course->id, 'page', 2, false); > * $references["cm4"] = $this->create_activity($course->id, 'forum', 3, false); > * @covers ::cm_visiblegroups $references["cm5"] = $this->create_activity($course->id, 'wiki', 3, false); > * @dataProvider basic_role_provider > * @param string $role the user role $user = $this->getDataGenerator()->create_user(); > * @param bool $expectedexception if it will expect an exception. $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher'); > */ $this->setUser($user); > public function test_cm_visiblegroups( > string $role = 'editingteacher', // Initialise stateupdates. > bool $expectedexception = false $courseformat = course_get_format($course->id); > ): void { $updates = new stateupdates($courseformat); > $this->basic_state_text( > 'cm_visiblegroups', // Execute the method. > $role, $actions = new stateactions(); > ['cm0', 'cm1', 'cm2', 'cm3'], $actions->section_move_after( > $expectedexception, $updates, > ['put' => 4], $course, > null, $this->translate_references($references, $sectiontomove), > null, $references[$targetsection] > null, ); > null, > 'groupmode', // Format results in a way we can compare easily. > VISIBLEGROUPS $results = $this->summarize_updates($updates); > ); > } // Validate we have all the expected entries. > $this->assertEquals(0, $results['create']['count']); > /** $this->assertEquals(0, $results['remove']['count']); > * Test for cm_separategroups // Moving a section puts: > * // - The course state. > * @covers ::cm_separategroups // - All sections state. > * @dataProvider basic_role_provider // - The cm states related to the moved and target sections. > * @param string $role the user role $this->assertEquals($totalputs, $results['put']['count']); > * @param bool $expectedexception if it will expect an exception. > */ // Course state should contain the sorted list of sections (section zero + 8 sections). > public function test_cm_separategroups( $finalsectionids = $this->translate_references($references, $finalorder); > string $role = 'editingteacher', $coursestate = reset($results['put']['course']); > bool $expectedexception = false $this->assertEquals($finalsectionids, $coursestate->sectionlist); > ): void { // All sections should be present in the update. > $this->basic_state_text( $this->assertCount(9, $results['put']['section']); > 'cm_separategroups', // Only cms from the affected sections should be updated. > $role, $cmids = $this->translate_references($references, $updatedcms); > ['cm0', 'cm1', 'cm2', 'cm3'], $cms = $results['put']['cm']; > $expectedexception, foreach ($cmids as $cmid) { > ['put' => 4], $this->assertArrayHasKey($cmid, $cms); > null, } > null, } > null, > null, /** > 'groupmode', * Provider for test_section_move_after. > SEPARATEGROUPS * > ); * @return array the testing scenarios > } */ > public function section_move_after_provider(): array { > /**
return [ 'Move sections down' => [ 'sectiontomove' => ['section2', 'section4'], 'targetsection' => 'section7', 'finalorder' => [ 'section0', 'section1', 'section3', 'section5', 'section6', 'section7', 'section2', 'section4', 'section8', ], 'updatedcms' => ['cm2', 'cm3'], 'totalputs' => 12, ], 'Move sections up' => [ 'sectiontomove' => ['section3', 'section5'], 'targetsection' => 'section1', 'finalorder' => [ 'section0', 'section1', 'section3', 'section5', 'section2', 'section4', 'section6', 'section7', 'section8', ], 'updatedcms' => ['cm0', 'cm1', 'cm4', 'cm5'], 'totalputs' => 14, ], 'Move sections in the middle' => [ 'sectiontomove' => ['section2', 'section5'], 'targetsection' => 'section3', 'finalorder' => [ 'section0', 'section1', 'section3', 'section2', 'section5', 'section4', 'section6', 'section7', 'section8', ], 'updatedcms' => ['cm2', 'cm3', 'cm4', 'cm5'], 'totalputs' => 14, ], 'Move sections on top' => [ 'sectiontomove' => ['section3', 'section5'], 'targetsection' => 'section0', 'finalorder' => [ 'section0', 'section3', 'section5', 'section1', 'section2', 'section4', 'section6', 'section7', 'section8', ], 'updatedcms' => ['cm4', 'cm5'], 'totalputs' => 12, ], 'Move sections on bottom' => [ 'sectiontomove' => ['section3', 'section5'], 'targetsection' => 'section8', 'finalorder' => [ 'section0', 'section1', 'section2', 'section4', 'section6', 'section7', 'section8', 'section3', 'section5', ], 'updatedcms' => ['cm4', 'cm5'], 'totalputs' => 12, ], ]; } /** * Test for section_move_after capability checks. * * @covers ::section_move_after * @dataProvider basic_role_provider * @param string $role the user role * @param bool $expectedexception if it will expect an exception. */ public function test_section_move_after_capabilities( string $role = 'editingteacher', bool $expectedexception = false ): void { $this->resetAfterTest(); // We want modules to be deleted for good. set_config('coursebinenable', 0, 'tool_recyclebin'); $info = $this->basic_state_text( 'section_move_after', $role, ['section2'], $expectedexception, ['put' => 9], null, 0, null, 0, null, 0, 'section0' ); } }