Proyectos de Subversion Moodle

Rev

Rev 1 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

<?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
    ): void {
        $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
     *
     * @covers ::cm_nogroups
     * @dataProvider basic_role_provider
     * @param string $role the user role
     * @param bool $expectedexception if it will expect an exception.
     */
    public function test_cm_nogroups(
        string $role = 'editingteacher',
        bool $expectedexception = false
    ): void {
        $this->basic_state_text(
            'cm_nogroups',
            $role,
            ['cm0', 'cm1', 'cm2', 'cm3'],
            $expectedexception,
            ['put' => 4],
            null,
            null,
            null,
            null,
            'groupmode',
            NOGROUPS
        );
    }

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

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

    /**
     * Test for section_move_after
     *
     * @covers ::section_move_after
     * @dataProvider section_move_after_provider
     * @param string[] $sectiontomove the sections to move
     * @param string $targetsection the target section reference
     * @param string[] $finalorder the final sections order
     * @param string[] $updatedcms the list of cms in the state updates
     * @param int $totalputs the total amount of put updates
     */
    public function test_section_move_after(
        array $sectiontomove,
        string $targetsection,
        array $finalorder,
        array $updatedcms,
        int $totalputs
    ): void {
        $this->resetAfterTest();

        $course = $this->create_course('topics', 8, []);

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

        // 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', 3, false);
        $references["cm5"] = $this->create_activity($course->id, 'wiki', 3, false);

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

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

        // Execute the method.
        $actions = new stateactions();
        $actions->section_move_after(
            $updates,
            $course,
            $this->translate_references($references, $sectiontomove),
            $references[$targetsection]
        );

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

        // Validate we have all the expected entries.
        $this->assertEquals(0, $results['create']['count']);
        $this->assertEquals(0, $results['remove']['count']);
        // Moving a section puts:
        // - The course state.
        // - All sections state.
        // - The cm states related to the moved and target sections.
        $this->assertEquals($totalputs, $results['put']['count']);

        // Course state should contain the sorted list of sections (section zero + 8 sections).
        $finalsectionids = $this->translate_references($references, $finalorder);
        $coursestate = reset($results['put']['course']);
        $this->assertEquals($finalsectionids, $coursestate->sectionlist);
        // All sections should be present in the update.
        $this->assertCount(9, $results['put']['section']);
        // Only cms from the affected sections should be updated.
        $cmids = $this->translate_references($references, $updatedcms);
        $cms = $results['put']['cm'];
        foreach ($cmids as $cmid) {
            $this->assertArrayHasKey($cmid, $cms);
        }
    }

    /**
     * Provider for test_section_move_after.
     *
     * @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'
        );
    }
}