Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_courseformat;
18
 
19
use course_modinfo;
20
use moodle_exception;
1441 ariadna 21
use ReflectionMethod;
1 efrain 22
use stdClass;
23
 
24
/**
25
 * Tests for the stateactions class.
26
 *
27
 * @package    core_courseformat
28
 * @category   test
29
 * @copyright  2021 Sara Arjona (sara@moodle.com)
30
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 * @coversDefaultClass \core_courseformat\stateactions
32
 */
1441 ariadna 33
final class stateactions_test extends \advanced_testcase {
1 efrain 34
    /**
35
     * Helper method to create an activity into a section and add it to the $sections and $activities arrays.
36
     *
37
     * @param int $courseid Course identifier where the activity will be added.
38
     * @param string $type Activity type ('forum', 'assign', ...).
39
     * @param int $section Section number where the activity will be added.
40
     * @param bool $visible Whether the activity will be visible or not.
41
     * @return int the activity cm id
42
     */
43
    private function create_activity(
44
        int $courseid,
45
        string $type,
46
        int $section,
47
        bool $visible = true
48
    ): int {
49
        $activity = $this->getDataGenerator()->create_module(
50
            $type,
51
            ['course' => $courseid],
52
            [
53
                'section' => $section,
54
                'visible' => $visible
55
            ]
56
        );
57
        return $activity->cmid;
58
    }
59
 
60
    /**
61
     * Helper to create a course and generate a section list.
62
     *
63
     * @param string $format the course format
64
     * @param int $sections the number of sections
65
     * @param int[] $hiddensections the section numbers to hide
66
     * @return stdClass the course object
67
     */
68
    private function create_course(string $format, int $sections, array $hiddensections): stdClass {
69
        global $DB;
70
 
71
        $course = $this->getDataGenerator()->create_course(['numsections' => $sections, 'format' => $format]);
72
        foreach ($hiddensections as $section) {
73
            set_section_visible($course->id, $section, 0);
74
        }
75
 
76
        return $course;
77
    }
78
 
79
    /**
80
     * Return an array if the course references.
81
     *
82
     * This method is used to create alias to sections and other stuff in the dataProviders.
83
     *
84
     * @param stdClass $course the course object
85
     * @return int[] a relation betwee all references and its element id
86
     */
87
    private function course_references(stdClass $course): array {
88
        global $DB;
89
 
90
        $references = [];
91
 
92
        $sectionrecords = $DB->get_records('course_sections', ['course' => $course->id]);
93
        foreach ($sectionrecords as $id => $section) {
94
            $references["section{$section->section}"] = $section->id;
95
        }
96
        $references['course'] = $course->id;
97
        $references['invalidsection'] = -1;
98
        $references['invalidcm'] = -1;
99
 
100
        return $references;
101
    }
102
 
103
    /**
104
     * Translate a references array into current ids.
105
     *
106
     * @param string[] $references the references list
107
     * @param string[] $values the values to translate
108
     * @return int[] the list of ids
109
     */
110
    private function translate_references(array $references, array $values): array {
111
        $result = [];
112
        foreach ($values as $value) {
113
            $result[] = $references[$value];
114
        }
115
        return $result;
116
    }
117
 
118
    /**
119
     * Generate a sorted and summarized list of an state updates message.
120
     *
121
     * It is important to note that the order in the update messages are not important in a real scenario
122
     * because each message affects a specific part of the course state. However, for the PHPUnit test
123
     * have them sorted and classified simplifies the asserts.
124
     *
125
     * @param stateupdates $updateobj the state updates object
126
     * @return array of all data updates.
127
     */
128
    private function summarize_updates(stateupdates $updateobj): array {
129
        // Check state returned after executing given action.
130
        $updatelist = $updateobj->jsonSerialize();
131
 
132
        // Initial summary structure.
133
        $result = [
134
            'create' => [
135
                'course' => [],
136
                'section' => [],
137
                'cm' => [],
138
                'count' => 0,
139
            ],
140
            'put' => [
141
                'course' => [],
142
                'section' => [],
143
                'cm' => [],
144
                'count' => 0,
145
            ],
146
            'remove' => [
147
                'course' => [],
148
                'section' => [],
149
                'cm' => [],
150
                'count' => 0,
151
            ],
152
        ];
153
        foreach ($updatelist as $update) {
154
            if (!isset($result[$update->action])) {
155
                $result[$update->action] = [
156
                    'course' => [],
157
                    'section' => [],
158
                    'cm' => [],
159
                    'count' => 0,
160
                ];
161
            }
162
            $elementid = $update->fields->id ?? 0;
163
            $result[$update->action][$update->name][$elementid] = $update->fields;
164
            $result[$update->action]['count']++;
165
        }
166
        return $result;
167
    }
168
 
169
    /**
170
     * Enrol, set and create the test user depending on the role name.
171
     *
172
     * @param stdClass $course the course data
173
     * @param string $rolename the testing role name
174
     */
175
    private function set_test_user_by_role(stdClass $course, string $rolename) {
176
        if ($rolename == 'admin') {
177
            $this->setAdminUser();
178
        } else {
179
            $user = $this->getDataGenerator()->create_user();
180
            if ($rolename != 'unenroled') {
181
                $this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
182
            }
183
            $this->setUser($user);
184
        }
185
    }
186
 
187
    /**
188
     * Test the behaviour course_state.
189
     *
190
     * @dataProvider get_state_provider
191
     * @covers ::course_state
192
     * @covers ::section_state
193
     * @covers ::cm_state
194
     *
195
     * @param string $format The course will be created with this course format.
196
     * @param string $role The role of the user that will execute the method.
197
     * @param string $method the method to call
198
     * @param array $params the ids, targetsection and targetcm to use as params
199
     * @param array $expectedresults List of the course module names expected after calling the method.
200
     * @param bool $expectedexception If this call will raise an exception.
201
     */
202
    public function test_get_state(
203
        string $format,
204
        string $role,
205
        string $method,
206
        array $params,
207
        array $expectedresults,
208
        bool $expectedexception = false
209
    ): void {
210
        $this->resetAfterTest();
211
 
212
        // Create a course with 3 sections, 1 of them hidden.
213
        $course = $this->create_course($format, 3, [2]);
214
 
215
        $references = $this->course_references($course);
216
 
217
        // Create and enrol user using given role.
218
        $this->set_test_user_by_role($course, $role);
219
 
1441 ariadna 220
        // Some formats, like social, can create some initial activity.
221
        $modninfo = course_modinfo::instance($course);
222
        $cms = $modninfo->get_cms();
223
        $count = 0;
224
        foreach ($cms as $cm) {
225
            $references["initialcm{$count}"] = $cm->id;
226
            $count++;
227
        }
228
 
1 efrain 229
        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
230
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
231
        $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
232
        $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
233
        $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
234
 
235
        if ($expectedexception) {
236
            $this->expectException(moodle_exception::class);
237
        }
238
 
239
        // Initialise stateupdates.
240
        $courseformat = course_get_format($course->id);
241
        $updates = new stateupdates($courseformat);
242
 
243
        // Execute given method.
244
        $actions = new stateactions();
245
        $actions->$method(
246
            $updates,
247
            $course,
248
            $this->translate_references($references, $params['ids']),
249
            $references[$params['targetsectionid']] ?? null,
250
            $references[$params['targetcmid']] ?? null
251
        );
252
 
253
        // Format results in a way we can compare easily.
254
        $results = $this->summarize_updates($updates);
255
 
256
        // The state actions does not use create or remove actions because they are designed
257
        // to refresh parts of the state.
258
        $this->assertEquals(0, $results['create']['count']);
259
        $this->assertEquals(0, $results['remove']['count']);
260
 
261
        // Validate we have all the expected entries.
262
        $expectedtotal = count($expectedresults['course']) + count($expectedresults['section']) + count($expectedresults['cm']);
263
        $this->assertEquals($expectedtotal, $results['put']['count']);
264
 
265
        // Validate course, section and cm.
266
        foreach ($expectedresults as $name => $referencekeys) {
267
            foreach ($referencekeys as $referencekey) {
268
                $this->assertArrayHasKey($references[$referencekey], $results['put'][$name]);
269
            }
270
        }
271
    }
272
 
273
    /**
274
     * Data provider for data request creation tests.
275
     *
276
     * @return array the testing scenarios
277
     */
1441 ariadna 278
    public static function get_state_provider(): array {
1 efrain 279
        return array_merge(
1441 ariadna 280
            static::course_state_provider('weeks'),
281
            static::course_state_provider('topics'),
282
            static::course_state_provider('social'),
283
            static::course_state_provider('singleactivity'),
284
            static::section_state_provider('weeks', 'admin'),
285
            static::section_state_provider('weeks', 'editingteacher'),
286
            static::section_state_provider('weeks', 'student'),
287
            static::section_state_provider('topics', 'admin'),
288
            static::section_state_provider('topics', 'editingteacher'),
289
            static::section_state_provider('topics', 'student'),
290
            static::section_state_provider('social', 'admin'),
291
            static::section_state_provider('social', 'editingteacher'),
292
            static::section_state_provider('social', 'student'),
293
            static::section_state_provider('singleactivity', 'admin'),
294
            static::section_state_provider('singleactivity', 'editingteacher'),
295
            static::section_state_provider('singleactivity', 'student'),
296
            static::cm_state_provider('weeks', 'admin'),
297
            static::cm_state_provider('weeks', 'editingteacher'),
298
            static::cm_state_provider('weeks', 'student'),
299
            static::cm_state_provider('topics', 'admin'),
300
            static::cm_state_provider('topics', 'editingteacher'),
301
            static::cm_state_provider('topics', 'student'),
302
            static::cm_state_provider('social', 'admin'),
303
            static::cm_state_provider('social', 'editingteacher'),
304
            static::cm_state_provider('social', 'student'),
305
            static::cm_state_provider('singleactivity', 'admin'),
306
            static::cm_state_provider('singleactivity', 'editingteacher'),
307
            static::cm_state_provider('singleactivity', 'student'),
1 efrain 308
        );
309
    }
310
 
311
    /**
312
     * Course state data provider.
313
     *
314
     * @param string $format the course format
315
     * @return array the testing scenarios
316
     */
1441 ariadna 317
    public static function course_state_provider(string $format): array {
318
        $expectedexception = ($format === 'singleactivity');
319
 
320
        $cms = ['cm0', 'cm1', 'cm2', 'cm3'];
321
        // All sections and cms that the user can access to.
322
        $usersections = ['section0', 'section1', 'section2', 'section3'];
323
 
324
        $studentcms = ['cm0'];
325
        if ($format === 'social') {
326
            $cms = ['initialcm0', 'cm0', 'cm1', 'cm2', 'cm3'];
327
            $studentcms = ['initialcm0', 'cm0'];
328
            $usersections = ['section0']; // Social format only uses section 0 (for all users).
329
        }
330
 
1 efrain 331
        return [
332
            // Tests for course_state.
333
            "admin $format course_state" => [
334
                'format' => $format,
335
                'role' => 'admin',
336
                'method' => 'course_state',
337
                'params' => [
338
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
339
                ],
340
                'expectedresults' => [
341
                    'course' => ['course'],
1441 ariadna 342
                    'section' => array_intersect($usersections, ['section0', 'section1', 'section2', 'section3']),
343
                    'cm' => $cms,
1 efrain 344
                ],
345
                'expectedexception' => $expectedexception,
346
            ],
347
            "editingteacher $format course_state" => [
348
                'format' => $format,
349
                'role' => 'editingteacher',
350
                'method' => 'course_state',
351
                'params' => [
352
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
353
                ],
354
                'expectedresults' => [
355
                    'course' => ['course'],
1441 ariadna 356
                    'section' => array_intersect($usersections, ['section0', 'section1', 'section2', 'section3']),
357
                    'cm' => $cms,
1 efrain 358
                ],
359
                'expectedexception' => $expectedexception,
360
            ],
361
            "student $format course_state" => [
362
                'format' => $format,
363
                'role' => 'student',
364
                'method' => 'course_state',
365
                'params' => [
366
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
367
                ],
368
                'expectedresults' => [
369
                    'course' => ['course'],
1441 ariadna 370
                    'section' => array_intersect($usersections, ['section0', 'section1', 'section3']),
371
                    'cm' => $studentcms,
1 efrain 372
                ],
373
                'expectedexception' => $expectedexception,
374
            ],
375
        ];
376
    }
377
 
378
    /**
379
     * Section state data provider.
380
     *
381
     * @param string $format the course format
382
     * @param string $role the user role
383
     * @return array the testing scenarios
384
     */
1441 ariadna 385
    public static function section_state_provider(string $format, string $role): array {
1 efrain 386
        // Social format will raise an exception and debug messages because it does not
387
        // use sections and it does not provide a renderer.
1441 ariadna 388
        $expectedexception = ($format === 'singleactivity');
1 efrain 389
 
390
        // All sections and cms that the user can access to.
391
        $usersections = ['section0', 'section1', 'section2', 'section3'];
392
        $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
393
        if ($role == 'student') {
394
            $usersections = ['section0', 'section1', 'section3'];
395
            $usercms = ['cm0'];
396
        }
1441 ariadna 397
        if ($format === 'social') {
398
            $usercms = ['initialcm0', ...$usercms];
399
            $usersections = ['section0']; // Social format only uses section 0 (for all users).
400
        }
1 efrain 401
 
402
        return [
403
            "$role $format section_state no section" => [
404
                'format' => $format,
405
                'role' => $role,
406
                'method' => 'section_state',
407
                'params' => [
408
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
409
                ],
410
                'expectedresults' => [],
411
                'expectedexception' => true,
412
            ],
413
            "$role $format section_state section 0" => [
414
                'format' => $format,
415
                'role' => $role,
416
                'method' => 'section_state',
417
                'params' => [
418
                    'ids' => ['section0'], 'targetsectionid' => null, 'targetcmid' => null
419
                ],
420
                'expectedresults' => [
421
                    'course' => [],
422
                    'section' => array_intersect(['section0'], $usersections),
1441 ariadna 423
                    'cm' => ($format == 'social') ? ['initialcm0'] : [],
1 efrain 424
                ],
425
                'expectedexception' => $expectedexception,
426
            ],
427
            "$role $format section_state visible section" => [
428
                'format' => $format,
429
                'role' => $role,
430
                'method' => 'section_state',
431
                'params' => [
432
                    'ids' => ['section1'], 'targetsectionid' => null, 'targetcmid' => null
433
                ],
434
                'expectedresults' => [
435
                    'course' => [],
436
                    'section' => array_intersect(['section1'], $usersections),
437
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
438
                ],
439
                'expectedexception' => $expectedexception,
440
            ],
441
            "$role $format section_state hidden section" => [
442
                'format' => $format,
443
                'role' => $role,
444
                'method' => 'section_state',
445
                'params' => [
446
                    'ids' => ['section2'], 'targetsectionid' => null, 'targetcmid' => null
447
                ],
448
                'expectedresults' => [
449
                    'course' => [],
450
                    'section' => array_intersect(['section2'], $usersections),
451
                    'cm' => array_intersect(['cm2', 'cm3'], $usercms),
452
                ],
453
                'expectedexception' => $expectedexception,
454
            ],
455
            "$role $format section_state several sections" => [
456
                'format' => $format,
457
                'role' => $role,
458
                'method' => 'section_state',
459
                'params' => [
460
                    'ids' => ['section1', 'section3'], 'targetsectionid' => null, 'targetcmid' => null
461
                ],
462
                'expectedresults' => [
463
                    'course' => [],
464
                    'section' => array_intersect(['section1', 'section3'], $usersections),
465
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
466
                ],
467
                'expectedexception' => $expectedexception,
468
            ],
469
            "$role $format section_state invalid section" => [
470
                'format' => $format,
471
                'role' => $role,
472
                'method' => 'section_state',
473
                'params' => [
474
                    'ids' => ['invalidsection'], 'targetsectionid' => null, 'targetcmid' => null
475
                ],
476
                'expectedresults' => [],
477
                'expectedexception' => true,
478
            ],
479
            "$role $format section_state using target section" => [
480
                'format' => $format,
481
                'role' => $role,
482
                'method' => 'section_state',
483
                'params' => [
484
                    'ids' => ['section1'], 'targetsectionid' => 'section3', 'targetcmid' => null
485
                ],
486
                'expectedresults' => [
487
                    'course' => [],
488
                    'section' => array_intersect(['section1', 'section3'], $usersections),
489
                    'cm' => array_intersect(['cm0', 'cm1'], $usercms),
490
                ],
491
                'expectedexception' => $expectedexception,
492
            ],
493
            "$role $format section_state using target targetcmid" => [
494
                'format' => $format,
495
                'role' => $role,
496
                'method' => 'section_state',
497
                'params' => [
498
                    'ids' => ['section3'], 'targetsectionid' => null, 'targetcmid' => 'cm1'
499
                ],
500
                'expectedresults' => [
501
                    'course' => [],
502
                    'section' => array_intersect(['section3'], $usersections),
503
                    'cm' => array_intersect(['cm1'], $usercms),
504
                ],
505
                'expectedexception' => $expectedexception,
506
            ],
507
        ];
508
    }
509
 
510
    /**
511
     * Course module state data provider.
512
     *
513
     * @param string $format the course format
514
     * @param string $role the user role
515
     * @return array the testing scenarios
516
     */
1441 ariadna 517
    public static function cm_state_provider(string $format, string $role): array {
1 efrain 518
        // All sections and cms that the user can access to.
519
        $usersections = ['section0', 'section1', 'section2', 'section3'];
520
        $usercms = ['cm0', 'cm1', 'cm2', 'cm3'];
521
        if ($role == 'student') {
522
            $usersections = ['section0', 'section1', 'section3'];
523
            $usercms = ['cm0'];
524
        }
1441 ariadna 525
        if ($format === 'social') {
526
            $usercms = ['initialcm0', ...$usercms];
527
            $usersections = ['section0']; // Social format only uses section 0 (for all users).
528
        }
1 efrain 529
 
530
        return [
531
            "$role $format cm_state no cms" => [
532
                'format' => $format,
533
                'role' => $role,
534
                'method' => 'cm_state',
535
                'params' => [
536
                    'ids' => [], 'targetsectionid' => null, 'targetcmid' => null
537
                ],
538
                'expectedresults' => [],
539
                'expectedexception' => true,
540
            ],
541
            "$role $format cm_state visible cm" => [
542
                'format' => $format,
543
                'role' => $role,
544
                'method' => 'cm_state',
545
                'params' => [
546
                    'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => null
547
                ],
548
                'expectedresults' => [
549
                    'course' => [],
550
                    'section' => array_intersect(['section1'], $usersections),
551
                    'cm' => array_intersect(['cm0'], $usercms),
552
                ],
553
                'expectedexception' => false,
554
            ],
555
            "$role $format cm_state hidden cm" => [
556
                'format' => $format,
557
                'role' => $role,
558
                'method' => 'cm_state',
559
                'params' => [
560
                    'ids' => ['cm1'], 'targetsectionid' => null, 'targetcmid' => null
561
                ],
562
                'expectedresults' => [
563
                    'course' => [],
564
                    'section' => array_intersect(['section1'], $usersections),
565
                    'cm' => array_intersect(['cm1'], $usercms),
566
                ],
567
                'expectedexception' => false,
568
            ],
569
            "$role $format cm_state several cm" => [
570
                'format' => $format,
571
                'role' => $role,
572
                'method' => 'cm_state',
573
                'params' => [
574
                    'ids' => ['cm0', 'cm2'], 'targetsectionid' => null, 'targetcmid' => null
575
                ],
576
                'expectedresults' => [
577
                    'course' => [],
578
                    'section' => array_intersect(['section1', 'section2'], $usersections),
579
                    'cm' => array_intersect(['cm0', 'cm2'], $usercms),
580
                ],
581
                'expectedexception' => false,
582
            ],
583
            "$role $format cm_state using targetsection" => [
584
                'format' => $format,
585
                'role' => $role,
586
                'method' => 'cm_state',
587
                'params' => [
588
                    'ids' => ['cm0'], 'targetsectionid' => 'section2', 'targetcmid' => null
589
                ],
590
                'expectedresults' => [
591
                    'course' => [],
592
                    'section' => array_intersect(['section1', 'section2'], $usersections),
593
                    'cm' => array_intersect(['cm0'], $usercms),
594
                ],
1441 ariadna 595
                'expectedexception' => ($format === 'singleactivity'),
1 efrain 596
            ],
597
            "$role $format cm_state using targetcm" => [
598
                'format' => $format,
599
                'role' => $role,
600
                'method' => 'cm_state',
601
                'params' => [
602
                    'ids' => ['cm0'], 'targetsectionid' => null, 'targetcmid' => 'cm3'
603
                ],
604
                'expectedresults' => [
605
                    'course' => [],
606
                    'section' => array_intersect(['section1', 'section2'], $usersections),
607
                    'cm' => array_intersect(['cm0', 'cm3'], $usercms),
608
                ],
609
                'expectedexception' => false,
610
            ],
611
            "$role $format cm_state using an invalid cm" => [
612
                'format' => $format,
613
                'role' => $role,
614
                'method' => 'cm_state',
615
                'params' => [
616
                    'ids' => ['invalidcm'], 'targetsectionid' => null, 'targetcmid' => null
617
                ],
618
                'expectedresults' => [],
619
                'expectedexception' => true,
620
            ],
621
        ];
622
    }
623
 
624
    /**
625
     * Internal method for testing a specific state action.
626
     *
627
     * @param string $method the method to test
628
     * @param string $role the user role
629
     * @param string[] $idrefs the sections or cms id references to be used as method params
630
     * @param bool $expectedexception whether the call should throw an exception
631
     * @param int[] $expectedtotal the expected total number of state indexed by put, remove and create
632
     * @param string|null $coursefield the course field to check
633
     * @param int|string|null $coursevalue the section field value
634
     * @param string|null $sectionfield the section field to check
635
     * @param int|string|null $sectionvalue the section field value
636
     * @param string|null $cmfield the cm field to check
637
     * @param int|string|null $cmvalue the cm field value
638
     * @param string|null $targetsection optional target section reference
639
     * @param string|null $targetcm optional target cm reference
640
     * @return array an array of elements to do extra validations (course, references, results)
641
     */
642
    protected function basic_state_text(
643
        string $method = 'section_hide',
644
        string $role = 'editingteacher',
645
        array $idrefs = [],
646
        bool $expectedexception = false,
647
        array $expectedtotals = [],
648
        ?string $coursefield = null,
649
        $coursevalue = 0,
650
        ?string $sectionfield = null,
651
        $sectionvalue = 0,
652
        ?string $cmfield = null,
653
        $cmvalue = 0,
654
        ?string $targetsection = null,
655
        ?string $targetcm = null
656
    ): array {
657
        $this->resetAfterTest();
658
 
659
        // Create a course with 3 sections, 1 of them hidden.
660
        $course = $this->create_course('topics', 3, [2]);
661
 
662
        $references = $this->course_references($course);
663
 
664
        $user = $this->getDataGenerator()->create_user();
665
        $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
666
        $this->setUser($user);
667
 
668
        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
669
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
670
        $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
671
        $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
672
        $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
673
        $references["cm4"] = $this->create_activity($course->id, 'forum', 2, false);
674
        $references["cm5"] = $this->create_activity($course->id, 'wiki', 2, false);
675
 
676
        if ($expectedexception) {
677
            $this->expectException(moodle_exception::class);
678
        }
679
 
680
        // Initialise stateupdates.
681
        $courseformat = course_get_format($course->id);
682
        $updates = new stateupdates($courseformat);
683
 
684
        // Execute the method.
685
        $actions = new stateactions();
686
        $actions->$method(
687
            $updates,
688
            $course,
689
            $this->translate_references($references, $idrefs),
690
            ($targetsection) ? $references[$targetsection] : null,
691
            ($targetcm) ? $references[$targetcm] : null,
692
        );
693
 
694
        // Format results in a way we can compare easily.
695
        $results = $this->summarize_updates($updates);
696
 
697
        // Validate we have all the expected entries.
698
        $this->assertEquals($expectedtotals['create'] ?? 0, $results['create']['count']);
699
        $this->assertEquals($expectedtotals['remove'] ?? 0, $results['remove']['count']);
700
        $this->assertEquals($expectedtotals['put'] ?? 0, $results['put']['count']);
701
 
702
        // Validate course, section and cm.
703
        if (!empty($coursefield)) {
704
            foreach ($results['put']['course'] as $courseid) {
705
                $this->assertEquals($coursevalue, $results['put']['course'][$courseid][$coursefield]);
706
            }
707
        }
708
        if (!empty($sectionfield)) {
709
            foreach ($results['put']['section'] as $section) {
710
                $this->assertEquals($sectionvalue, $section->$sectionfield);
711
            }
712
        }
713
        if (!empty($cmfield)) {
714
            foreach ($results['put']['cm'] as $cm) {
715
                $this->assertEquals($cmvalue, $cm->$cmfield);
716
            }
717
        }
718
        return [
719
            'course' => $course,
720
            'references' => $references,
721
            'results' => $results,
722
        ];
723
    }
724
 
725
    /**
726
     * Test for section_hide
727
     *
728
     * @covers ::section_hide
729
     * @dataProvider basic_role_provider
730
     * @param string $role the user role
731
     * @param bool $expectedexception if it will expect an exception.
732
     */
733
    public function test_section_hide(
734
        string $role = 'editingteacher',
735
        bool $expectedexception = false
736
    ): void {
737
        $this->basic_state_text(
738
            'section_hide',
739
            $role,
740
            ['section1', 'section2', 'section3'],
741
            $expectedexception,
742
            ['put' => 9],
743
            null,
744
            null,
745
            'visible',
746
            0,
747
            null,
748
            null
749
        );
750
    }
751
 
752
    /**
753
     * Test for section_hide
754
     *
755
     * @covers ::section_show
756
     * @dataProvider basic_role_provider
757
     * @param string $role the user role
758
     * @param bool $expectedexception if it will expect an exception.
759
     */
760
    public function test_section_show(
761
        string $role = 'editingteacher',
762
        bool $expectedexception = false
763
    ): void {
764
        $this->basic_state_text(
765
            'section_show',
766
            $role,
767
            ['section1', 'section2', 'section3'],
768
            $expectedexception,
769
            ['put' => 9],
770
            null,
771
            null,
772
            'visible',
773
            1,
774
            null,
775
            null
776
        );
777
    }
778
 
779
    /**
780
     * Test for cm_show
781
     *
782
     * @covers ::cm_show
783
     * @dataProvider basic_role_provider
784
     * @param string $role the user role
785
     * @param bool $expectedexception if it will expect an exception.
786
     */
787
    public function test_cm_show(
788
        string $role = 'editingteacher',
789
        bool $expectedexception = false
790
    ): void {
791
        $this->basic_state_text(
792
            'cm_show',
793
            $role,
794
            ['cm0', 'cm1', 'cm2', 'cm3'],
795
            $expectedexception,
796
            ['put' => 4],
797
            null,
798
            null,
799
            null,
800
            null,
801
            'visible',
802
            1
803
        );
804
    }
805
 
806
    /**
807
     * Test for cm_hide
808
     *
809
     * @covers ::cm_hide
810
     * @dataProvider basic_role_provider
811
     * @param string $role the user role
812
     * @param bool $expectedexception if it will expect an exception.
813
     */
814
    public function test_cm_hide(
815
        string $role = 'editingteacher',
816
        bool $expectedexception = false
817
    ): void {
818
        $this->basic_state_text(
819
            'cm_hide',
820
            $role,
821
            ['cm0', 'cm1', 'cm2', 'cm3'],
822
            $expectedexception,
823
            ['put' => 4],
824
            null,
825
            null,
826
            null,
827
            null,
828
            'visible',
829
 
830
        );
831
    }
832
 
833
    /**
834
     * Test for cm_stealth
835
     *
836
     * @covers ::cm_stealth
837
     * @dataProvider basic_role_provider
838
     * @param string $role the user role
839
     * @param bool $expectedexception if it will expect an exception.
840
     */
841
    public function test_cm_stealth(
842
        string $role = 'editingteacher',
843
        bool $expectedexception = false
844
    ): void {
845
        set_config('allowstealth', 1);
846
        $this->basic_state_text(
847
            'cm_stealth',
848
            $role,
849
            ['cm0', 'cm1', 'cm2', 'cm3'],
850
            $expectedexception,
851
            ['put' => 4],
852
            null,
853
            null,
854
            null,
855
            null,
856
            'stealth',
857
            1
858
        );
859
        // Disable stealth.
860
        set_config('allowstealth', 0);
861
        // When stealth are disabled the validation is a but more complex because they depends
862
        // also on the section visibility (legacy stealth).
863
        $this->basic_state_text(
864
            'cm_stealth',
865
            $role,
866
            ['cm0', 'cm1'],
867
            $expectedexception,
868
            ['put' => 2],
869
            null,
870
            null,
871
            null,
872
            null,
873
            'stealth',
874
 
875
        );
876
        $this->basic_state_text(
877
            'cm_stealth',
878
            $role,
879
            ['cm2', 'cm3'],
880
            $expectedexception,
881
            ['put' => 2],
882
            null,
883
            null,
884
            null,
885
            null,
886
            'stealth',
887
            1
888
        );
889
    }
890
 
891
    /**
892
     * Data provider for basic role tests.
893
     *
894
     * @return array the testing scenarios
895
     */
1441 ariadna 896
    public static function basic_role_provider(): array {
1 efrain 897
        return [
898
            'editingteacher' => [
899
                'role' => 'editingteacher',
900
                'expectedexception' => false,
901
            ],
902
            'teacher' => [
903
                'role' => 'teacher',
904
                'expectedexception' => true,
905
            ],
906
            'student' => [
907
                'role' => 'student',
908
                'expectedexception' => true,
909
            ],
910
            'guest' => [
911
                'role' => 'guest',
912
                'expectedexception' => true,
913
            ],
914
        ];
915
    }
916
 
917
    /**
918
     * Duplicate course module method.
919
     *
920
     * @covers ::cm_duplicate
921
     * @dataProvider cm_duplicate_provider
922
     * @param string $targetsection the target section (empty for none)
923
     * @param bool $validcms if uses valid cms
924
     * @param string $role the current user role name
925
     * @param bool $expectedexception if the test will raise an exception
926
     */
927
    public function test_cm_duplicate(
928
        string $targetsection = '',
929
        bool $validcms = true,
930
        string $role = 'admin',
931
        bool $expectedexception = false
11 efrain 932
    ): void {
1 efrain 933
        $this->resetAfterTest();
934
 
935
        // Create a course with 3 sections.
936
        $course = $this->create_course('topics', 3, []);
937
 
938
        $references = $this->course_references($course);
939
 
940
        // Create and enrol user using given role.
941
        $this->set_test_user_by_role($course, $role);
942
 
943
        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
944
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
945
        $references["cm1"] = $this->create_activity($course->id, 'page', 2, false);
946
 
947
        if ($expectedexception) {
948
            $this->expectException(moodle_exception::class);
949
        }
950
 
951
        // Initialise stateupdates.
952
        $courseformat = course_get_format($course->id);
953
        $updates = new stateupdates($courseformat);
954
 
955
        // Execute method.
956
        $targetsectionid = (!empty($targetsection)) ? $references[$targetsection] : null;
957
        $cmrefs = ($validcms) ? ['cm0', 'cm1'] : ['invalidcm'];
958
        $actions = new stateactions();
959
        $actions->cm_duplicate(
960
            $updates,
961
            $course,
962
            $this->translate_references($references, $cmrefs),
963
            $targetsectionid,
964
        );
965
 
966
        // Check the new elements in the course structure.
967
        $originalsections = [
968
            'assign' => $references['section1'],
969
            'page' => $references['section2'],
970
        ];
971
        $modinfo = course_modinfo::instance($course);
972
        $cms = $modinfo->get_cms();
973
        $i = 0;
974
        foreach ($cms as $cmid => $cminfo) {
975
            if ($cmid == $references['cm0'] || $cmid == $references['cm1']) {
976
                continue;
977
            }
978
            $references["newcm$i"] = $cmid;
979
            if ($targetsectionid) {
980
                $this->assertEquals($targetsectionid, $cminfo->section);
981
            } else {
982
                $this->assertEquals($originalsections[$cminfo->modname], $cminfo->section);
983
            }
984
            $i++;
985
        }
986
 
987
        // Check the resulting updates.
988
        $results = $this->summarize_updates($updates);
989
 
990
        if ($targetsectionid) {
991
            $this->assertArrayHasKey($references[$targetsection], $results['put']['section']);
992
        } else {
993
            $this->assertArrayHasKey($references['section1'], $results['put']['section']);
994
            $this->assertArrayHasKey($references['section2'], $results['put']['section']);
995
        }
996
        $countcms = ($targetsection == 'section3' || $targetsection === '') ? 2 : 3;
997
        $this->assertCount($countcms, $results['put']['cm']);
998
        $this->assertArrayHasKey($references['newcm0'], $results['put']['cm']);
999
        $this->assertArrayHasKey($references['newcm1'], $results['put']['cm']);
1000
    }
1001
 
1002
    /**
1003
     * Duplicate course module data provider.
1004
     *
1005
     * @return array the testing scenarios
1006
     */
1441 ariadna 1007
    public static function cm_duplicate_provider(): array {
1 efrain 1008
        return [
1009
            'valid cms without target section' => [
1010
                'targetsection' => '',
1011
                'validcms' => true,
1012
                'role' => 'admin',
1013
                'expectedexception' => false,
1014
            ],
1015
            'valid cms targeting an empty section' => [
1016
                'targetsection' => 'section3',
1017
                'validcms' => true,
1018
                'role' => 'admin',
1019
                'expectedexception' => false,
1020
            ],
1021
            'valid cms targeting a section with activities' => [
1022
                'targetsection' => 'section2',
1023
                'validcms' => true,
1024
                'role' => 'admin',
1025
                'expectedexception' => false,
1026
            ],
1027
            'invalid cms without target section' => [
1028
                'targetsection' => '',
1029
                'validcms' => false,
1030
                'role' => 'admin',
1031
                'expectedexception' => true,
1032
            ],
1033
            'invalid cms with target section' => [
1034
                'targetsection' => 'section3',
1035
                'validcms' => false,
1036
                'role' => 'admin',
1037
                'expectedexception' => true,
1038
            ],
1039
            'student role with target section' => [
1040
                'targetsection' => 'section3',
1041
                'validcms' => true,
1042
                'role' => 'student',
1043
                'expectedexception' => true,
1044
            ],
1045
            'student role without target section' => [
1046
                'targetsection' => '',
1047
                'validcms' => true,
1048
                'role' => 'student',
1049
                'expectedexception' => true,
1050
            ],
1051
            'unrenolled user with target section' => [
1052
                'targetsection' => 'section3',
1053
                'validcms' => true,
1054
                'role' => 'unenroled',
1055
                'expectedexception' => true,
1056
            ],
1057
            'unrenolled user without target section' => [
1058
                'targetsection' => '',
1059
                'validcms' => true,
1060
                'role' => 'unenroled',
1061
                'expectedexception' => true,
1062
            ],
1063
        ];
1064
    }
1065
 
1066
    /**
1067
     * Test for cm_delete
1068
     *
1069
     * @covers ::cm_delete
1070
     * @dataProvider basic_role_provider
1071
     * @param string $role the user role
1072
     * @param bool $expectedexception if it will expect an exception.
1073
     */
1074
    public function test_cm_delete(
1075
        string $role = 'editingteacher',
1076
        bool $expectedexception = false
1077
    ): void {
1078
        $this->resetAfterTest();
1079
        // We want modules to be deleted for good.
1080
        set_config('coursebinenable', 0, 'tool_recyclebin');
1081
 
1082
        $info = $this->basic_state_text(
1083
            'cm_delete',
1084
            $role,
1085
            ['cm2', 'cm3'],
1086
            $expectedexception,
1087
            ['remove' => 2, 'put' => 1],
1088
        );
1089
 
1090
        $course = $info['course'];
1091
        $references = $info['references'];
1092
        $results = $info['results'];
1093
        $courseformat = course_get_format($course->id);
1094
 
1095
        $this->assertArrayNotHasKey($references['cm0'], $results['remove']['cm']);
1096
        $this->assertArrayNotHasKey($references['cm1'], $results['remove']['cm']);
1097
        $this->assertArrayHasKey($references['cm2'], $results['remove']['cm']);
1098
        $this->assertArrayHasKey($references['cm3'], $results['remove']['cm']);
1099
        $this->assertArrayNotHasKey($references['cm4'], $results['remove']['cm']);
1100
        $this->assertArrayNotHasKey($references['cm5'], $results['remove']['cm']);
1101
 
1102
        // Check the new section cm list.
1103
        $newcmlist = $this->translate_references($references, ['cm4', 'cm5']);
1104
        $section = $results['put']['section'][$references['section2']];
1105
        $this->assertEquals($newcmlist, $section->cmlist);
1106
 
1107
        // Check activities are deleted.
1108
        $modinfo = $courseformat->get_modinfo();
1109
        $cms = $modinfo->get_cms();
1110
        $this->assertArrayHasKey($references['cm0'], $cms);
1111
        $this->assertArrayHasKey($references['cm1'], $cms);
1112
        $this->assertArrayNotHasKey($references['cm2'], $cms);
1113
        $this->assertArrayNotHasKey($references['cm3'], $cms);
1114
        $this->assertArrayHasKey($references['cm4'], $cms);
1115
        $this->assertArrayHasKey($references['cm5'], $cms);
1116
    }
1117
 
1118
    /**
1119
     * Test for cm_moveright
1120
     *
1121
     * @covers ::cm_moveright
1122
     * @dataProvider basic_role_provider
1123
     * @param string $role the user role
1124
     * @param bool $expectedexception if it will expect an exception.
1125
     */
1126
    public function test_cm_moveright(
1127
        string $role = 'editingteacher',
1128
        bool $expectedexception = false
1129
    ): void {
1130
        $this->basic_state_text(
1131
            'cm_moveright',
1132
            $role,
1133
            ['cm0', 'cm1', 'cm2', 'cm3'],
1134
            $expectedexception,
1135
            ['put' => 4],
1136
            null,
1137
            null,
1138
            null,
1139
            null,
1140
            'indent',
1141
            1
1142
        );
1143
    }
1144
 
1145
    /**
1146
     * Test for cm_moveleft
1147
     *
1148
     * @covers ::cm_moveleft
1149
     * @dataProvider basic_role_provider
1150
     * @param string $role the user role
1151
     * @param bool $expectedexception if it will expect an exception.
1152
     */
1153
    public function test_cm_moveleft(
1154
        string $role = 'editingteacher',
1155
        bool $expectedexception = false
1156
    ): void {
1157
        $this->basic_state_text(
1158
            'cm_moveleft',
1159
            $role,
1160
            ['cm0', 'cm1', 'cm2', 'cm3'],
1161
            $expectedexception,
1162
            ['put' => 4],
1163
            null,
1164
            null,
1165
            null,
1166
            null,
1167
            'indent',
1168
 
1169
        );
1170
    }
1171
 
1172
    /**
1173
     * Test for cm_nogroups
1174
     *
1175
     * @covers ::cm_nogroups
1176
     * @dataProvider basic_role_provider
1177
     * @param string $role the user role
1178
     * @param bool $expectedexception if it will expect an exception.
1179
     */
1180
    public function test_cm_nogroups(
1181
        string $role = 'editingteacher',
1182
        bool $expectedexception = false
1183
    ): void {
1184
        $this->basic_state_text(
1185
            'cm_nogroups',
1186
            $role,
1187
            ['cm0', 'cm1', 'cm2', 'cm3'],
1188
            $expectedexception,
1189
            ['put' => 4],
1190
            null,
1191
            null,
1192
            null,
1193
            null,
1194
            'groupmode',
1195
            NOGROUPS
1196
        );
1197
    }
1198
 
1199
    /**
1200
     * Test for cm_visiblegroups
1201
     *
1202
     * @covers ::cm_visiblegroups
1203
     * @dataProvider basic_role_provider
1204
     * @param string $role the user role
1205
     * @param bool $expectedexception if it will expect an exception.
1206
     */
1207
    public function test_cm_visiblegroups(
1208
        string $role = 'editingteacher',
1209
        bool $expectedexception = false
1210
    ): void {
1211
        $this->basic_state_text(
1212
            'cm_visiblegroups',
1213
            $role,
1214
            ['cm0', 'cm1', 'cm2', 'cm3'],
1215
            $expectedexception,
1216
            ['put' => 4],
1217
            null,
1218
            null,
1219
            null,
1220
            null,
1221
            'groupmode',
1222
            VISIBLEGROUPS
1223
        );
1224
    }
1225
 
1226
    /**
1227
     * Test for cm_separategroups
1228
     *
1229
     * @covers ::cm_separategroups
1230
     * @dataProvider basic_role_provider
1231
     * @param string $role the user role
1232
     * @param bool $expectedexception if it will expect an exception.
1233
     */
1234
    public function test_cm_separategroups(
1235
        string $role = 'editingteacher',
1236
        bool $expectedexception = false
1237
    ): void {
1238
        $this->basic_state_text(
1239
            'cm_separategroups',
1240
            $role,
1241
            ['cm0', 'cm1', 'cm2', 'cm3'],
1242
            $expectedexception,
1243
            ['put' => 4],
1244
            null,
1245
            null,
1246
            null,
1247
            null,
1248
            'groupmode',
1249
            SEPARATEGROUPS
1250
        );
1251
    }
1252
 
1253
    /**
1254
     * Test for section_move_after
1255
     *
1256
     * @covers ::section_move_after
1257
     * @dataProvider section_move_after_provider
1258
     * @param string[] $sectiontomove the sections to move
1259
     * @param string $targetsection the target section reference
1260
     * @param string[] $finalorder the final sections order
1261
     * @param string[] $updatedcms the list of cms in the state updates
1262
     * @param int $totalputs the total amount of put updates
1263
     */
1264
    public function test_section_move_after(
1265
        array $sectiontomove,
1266
        string $targetsection,
1267
        array $finalorder,
1268
        array $updatedcms,
1269
        int $totalputs
1270
    ): void {
1271
        $this->resetAfterTest();
1272
 
1273
        $course = $this->create_course('topics', 8, []);
1274
 
1275
        $references = $this->course_references($course);
1276
 
1277
        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
1278
        $references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
1279
        $references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
1280
        $references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
1281
        $references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
1282
        $references["cm4"] = $this->create_activity($course->id, 'forum', 3, false);
1283
        $references["cm5"] = $this->create_activity($course->id, 'wiki', 3, false);
1284
 
1285
        $user = $this->getDataGenerator()->create_user();
1286
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
1287
        $this->setUser($user);
1288
 
1289
        // Initialise stateupdates.
1290
        $courseformat = course_get_format($course->id);
1291
        $updates = new stateupdates($courseformat);
1292
 
1293
        // Execute the method.
1294
        $actions = new stateactions();
1295
        $actions->section_move_after(
1296
            $updates,
1297
            $course,
1298
            $this->translate_references($references, $sectiontomove),
1299
            $references[$targetsection]
1300
        );
1301
 
1302
        // Format results in a way we can compare easily.
1303
        $results = $this->summarize_updates($updates);
1304
 
1305
        // Validate we have all the expected entries.
1306
        $this->assertEquals(0, $results['create']['count']);
1307
        $this->assertEquals(0, $results['remove']['count']);
1308
        // Moving a section puts:
1309
        // - The course state.
1310
        // - All sections state.
1311
        // - The cm states related to the moved and target sections.
1312
        $this->assertEquals($totalputs, $results['put']['count']);
1313
 
1314
        // Course state should contain the sorted list of sections (section zero + 8 sections).
1315
        $finalsectionids = $this->translate_references($references, $finalorder);
1316
        $coursestate = reset($results['put']['course']);
1317
        $this->assertEquals($finalsectionids, $coursestate->sectionlist);
1318
        // All sections should be present in the update.
1319
        $this->assertCount(9, $results['put']['section']);
1320
        // Only cms from the affected sections should be updated.
1321
        $cmids = $this->translate_references($references, $updatedcms);
1322
        $cms = $results['put']['cm'];
1323
        foreach ($cmids as $cmid) {
1324
            $this->assertArrayHasKey($cmid, $cms);
1325
        }
1326
    }
1327
 
1328
    /**
1329
     * Provider for test_section_move_after.
1330
     *
1331
     * @return array the testing scenarios
1332
     */
1441 ariadna 1333
    public static function section_move_after_provider(): array {
1 efrain 1334
        return [
1335
            'Move sections down' => [
1336
                'sectiontomove' => ['section2', 'section4'],
1337
                'targetsection' => 'section7',
1338
                'finalorder' => [
1339
                    'section0',
1340
                    'section1',
1341
                    'section3',
1342
                    'section5',
1343
                    'section6',
1344
                    'section7',
1345
                    'section2',
1346
                    'section4',
1347
                    'section8',
1348
                ],
1349
                'updatedcms' => ['cm2', 'cm3'],
1350
                'totalputs' => 12,
1351
            ],
1352
            'Move sections up' => [
1353
                'sectiontomove' => ['section3', 'section5'],
1354
                'targetsection' => 'section1',
1355
                'finalorder' => [
1356
                    'section0',
1357
                    'section1',
1358
                    'section3',
1359
                    'section5',
1360
                    'section2',
1361
                    'section4',
1362
                    'section6',
1363
                    'section7',
1364
                    'section8',
1365
                ],
1366
                'updatedcms' => ['cm0', 'cm1', 'cm4', 'cm5'],
1367
                'totalputs' => 14,
1368
            ],
1369
            'Move sections in the middle' => [
1370
                'sectiontomove' => ['section2', 'section5'],
1371
                'targetsection' => 'section3',
1372
                'finalorder' => [
1373
                    'section0',
1374
                    'section1',
1375
                    'section3',
1376
                    'section2',
1377
                    'section5',
1378
                    'section4',
1379
                    'section6',
1380
                    'section7',
1381
                    'section8',
1382
                ],
1383
                'updatedcms' => ['cm2', 'cm3', 'cm4', 'cm5'],
1384
                'totalputs' => 14,
1385
            ],
1386
            'Move sections on top' => [
1387
                'sectiontomove' => ['section3', 'section5'],
1388
                'targetsection' => 'section0',
1389
                'finalorder' => [
1390
                    'section0',
1391
                    'section3',
1392
                    'section5',
1393
                    'section1',
1394
                    'section2',
1395
                    'section4',
1396
                    'section6',
1397
                    'section7',
1398
                    'section8',
1399
                ],
1400
                'updatedcms' => ['cm4', 'cm5'],
1401
                'totalputs' => 12,
1402
            ],
1403
            'Move sections on bottom' => [
1404
                'sectiontomove' => ['section3', 'section5'],
1405
                'targetsection' => 'section8',
1406
                'finalorder' => [
1407
                    'section0',
1408
                    'section1',
1409
                    'section2',
1410
                    'section4',
1411
                    'section6',
1412
                    'section7',
1413
                    'section8',
1414
                    'section3',
1415
                    'section5',
1416
                ],
1417
                'updatedcms' => ['cm4', 'cm5'],
1418
                'totalputs' => 12,
1419
            ],
1420
        ];
1421
    }
1422
 
1423
    /**
1441 ariadna 1424
     * Test course module move and subsection move.
1425
     *
1426
     * @covers ::cm_move
1427
     * @dataProvider cm_move_provider
1428
     * @param string[] $cmtomove the sections to move
1429
     * @param string $targetsection
1430
     * @param string[] $expectedcoursetree expected course tree
1431
     * @param string|null $expectedexception if it will expect an exception.
1432
     */
1433
    public function test_cm_move(
1434
        array $cmtomove,
1435
        string $targetsection,
1436
        array $expectedcoursetree,
1437
        ?string $expectedexception = null
1438
    ): void {
1439
        $this->resetAfterTest();
1440
        $course = $this->create_course('topics', 4, []);
1441
 
1442
        $subsection1 = $this->getDataGenerator()->create_module(
1443
            'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection1']
1444
        );
1445
        $subsection2 = $this->getDataGenerator()->create_module(
1446
            'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection2']
1447
        );
1448
        $modinfo = get_fast_modinfo($course);
1449
        $subsection1info = $modinfo->get_section_info_by_component('mod_subsection', $subsection1->id);
1450
        $subsection2info = $modinfo->get_section_info_by_component('mod_subsection', $subsection2->id);
1451
 
1452
        $references = $this->course_references($course);
1453
        // Add some activities to the course. One visible and one hidden in both sections 1 and 2.
1454
        $references["cm0"] = $this->create_activity($course->id, 'assign', 0);
1455
        $references["cm1"] = $this->create_activity($course->id, 'page', 2);
1456
        $references["cm2"] = $this->create_activity($course->id, 'forum', $subsection1info->sectionnum);
1457
        $references["subsection1"] = intval($subsection1->cmid);
1458
        $references["subsection2"] = intval($subsection2->cmid);
1459
        $references["subsection1sectionid"] = $subsection1info->id;
1460
        $references["subsection2sectionid"] = $subsection2info->id;
1461
        $user = $this->getDataGenerator()->create_user();
1462
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher');
1463
        $this->setUser($user);
1464
 
1465
        // Initialise stateupdates.
1466
        $courseformat = course_get_format($course->id);
1467
        $updates = new stateupdates($courseformat);
1468
 
1469
        if ($expectedexception) {
1470
            $this->expectExceptionMessage($expectedexception);
1471
        }
1472
        // Execute the method.
1473
        $actions = new stateactions();
1474
        // We do this to make sure we can reference subsection1 in the course tree (and not just section5 as subsection1 is
1475
        // both a module and a subsection).
1476
        if (str_starts_with($targetsection, 'subsection')) {
1477
            $targetsection = $targetsection . 'sectionid';
1478
        }
1479
        $actions->cm_move(
1480
            $updates,
1481
            $course,
1482
            $this->translate_references($references, $cmtomove),
1483
            $references[$targetsection]
1484
        );
1485
 
1486
        $coursetree = $this->get_course_tree($course, $references);
1487
        $this->assertEquals($expectedcoursetree, $coursetree);
1488
    }
1489
 
1490
    /**
1491
     * Get Course tree for later comparison.
1492
     *
1493
     * @param stdClass $course
1494
     * @param array $references
1495
     * @return array
1496
     */
1497
    private function get_course_tree(stdClass $course, array $references): array {
1498
        $coursetree = [];
1499
        $modinfo = get_fast_modinfo($course); // Get refreshed version.
1500
 
1501
        $allsections = $modinfo->get_listed_section_info_all();
1502
        $cmidstoref = array_flip($references);
1503
        foreach ($allsections as $sectioninfo) {
1504
            $sectionkey = 'section' . $sectioninfo->sectionnum;
1505
            $coursetree[$sectionkey] = [];
1506
            if (empty(trim($sectioninfo->sequence))) {
1507
                continue;
1508
            }
1509
            $cmids = explode(",", $sectioninfo->sequence);
1510
            foreach ($cmids as $cmid) {
1511
                $cm = $modinfo->get_cm($cmid);
1512
                $delegatedsection = $cm->get_delegated_section_info();
1513
 
1514
                // Course modules without a delegated section are included as activities.
1515
                if (!$delegatedsection) {
1516
                    $coursetree[$sectionkey][] = $cmidstoref[$cmid];
1517
                    continue;
1518
                }
1519
 
1520
                // Course modules with a delegated are included as a section, not as an activity.
1521
                $delegatedsectionkey = $delegatedsection->name; // We gave it a name, so let's use it as key.
1522
                $coursetree[$sectionkey][$delegatedsectionkey] = [];
1523
 
1524
                if (empty(trim($delegatedsection->sequence))) {
1525
                    continue;
1526
                }
1527
                $delegatedcmids = explode(",", $delegatedsection->sequence);
1528
                foreach ($delegatedcmids as $dcmid) {
1529
                    $coursetree[$sectionkey][$delegatedsectionkey][] = $cmidstoref[$dcmid];
1530
                }
1531
 
1532
            }
1533
        }
1534
        return $coursetree;
1535
    }
1536
 
1537
    /**
1538
     * Provider for test_section_move.
1539
     *
1540
     *
1541
     * The original coursetree looks like this:
1542
     * 'coursetree' => [
1543
     *    'section0' => ['cm0'],
1544
     *    'section1' => ['subsection1' => ['cm2'],'subsection2' => []],
1545
     *    'section2' => ['cm1'],
1546
     *    'section3' => [],
1547
     *    'section4' => [],
1548
     * ],
1549
     *
1550
     * @return array the testing scenarios
1551
     */
1552
    public static function cm_move_provider(): array {
1553
        return [
1554
            'Move module into section2' => [
1555
                'cmtomove' => ['cm0'],
1556
                'targetsection' => 'section2',
1557
                'expectedcoursetree' => [
1558
                    'section0' => [],
1559
                    'section1' => ['subsection1' => ['cm2'], 'subsection2' => []],
1560
                    'section2' => ['cm1', 'cm0'],
1561
                    'section3' => [],
1562
                    'section4' => [],
1563
                ],
1564
            ],
1565
            'Move subsection into another subsection' => [
1566
                'cmtomove' => ['subsection1'], // When moving a subsection we actually move the delegated module.
1567
                'targetsection' => 'subsection2',
1568
                'expectedcoursetree' => [],
1569
                'expectedexception' => 'error/subsectionmoveerror',
1570
            ],
1571
            'Move module into subsection' => [
1572
                'cmtomove' => ['cm1'],
1573
                'targetsection' => 'subsection1',
1574
                'expectedcoursetree' => [
1575
                    'section0' => ['cm0'],
1576
                    'section1' => ['subsection1' => ['cm2', 'cm1'], 'subsection2' => []],
1577
                    'section2' => [],
1578
                    'section3' => [],
1579
                    'section4' => [],
1580
                ],
1581
            ],
1582
        ];
1583
    }
1584
 
1585
    /**
1 efrain 1586
     * Test for section_move_after capability checks.
1587
     *
1588
     * @covers ::section_move_after
1589
     * @dataProvider basic_role_provider
1590
     * @param string $role the user role
1591
     * @param bool $expectedexception if it will expect an exception.
1592
     */
1593
    public function test_section_move_after_capabilities(
1594
        string $role = 'editingteacher',
1595
        bool $expectedexception = false
1596
    ): void {
1597
        $this->resetAfterTest();
1598
        // We want modules to be deleted for good.
1599
        set_config('coursebinenable', 0, 'tool_recyclebin');
1600
 
1601
        $info = $this->basic_state_text(
1602
            'section_move_after',
1603
            $role,
1604
            ['section2'],
1605
            $expectedexception,
1606
            ['put' => 9],
1607
            null,
1608
            0,
1609
            null,
1610
            0,
1611
            null,
1612
            0,
1613
            'section0'
1614
        );
1615
    }
1441 ariadna 1616
 
1617
    /**
1618
     * Test that set_cm_indentation on activities with a delegated section.
1619
     *
1620
     * @covers ::set_cm_indentation
1621
     */
1622
    public function test_set_cm_indentation_delegated_section(): void {
1623
        global $DB;
1624
 
1625
        $this->resetAfterTest();
1626
        $this->setAdminUser();
1627
 
1628
        $course = $this->getDataGenerator()->create_course();
1629
        $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]);
1630
        $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]);
1631
        $this->setAdminUser();
1632
 
1633
        // Initialise stateupdates.
1634
        $courseformat = course_get_format($course->id);
1635
 
1636
        // Execute given method.
1637
        $updates = new stateupdates($courseformat);
1638
        $actions = new stateactions();
1639
        $actions->cm_moveright(
1640
            $updates,
1641
            $course,
1642
            [$subsection->cmid, $otheractvity->cmid],
1643
        );
1644
 
1645
        // Format results in a way we can compare easily.
1646
        $results = $this->summarize_updates($updates);
1647
 
1648
        // The state actions does not use create or remove actions because they are designed
1649
        // to refresh parts of the state.
1650
        $this->assertEquals(0, $results['create']['count']);
1651
        $this->assertEquals(0, $results['remove']['count']);
1652
 
1653
        // Mod subsection should be ignored.
1654
        $this->assertEquals(1, $results['put']['count']);
1655
 
1656
        // Validate course, section and cm.
1657
        $this->assertArrayHasKey($otheractvity->cmid, $results['put']['cm']);
1658
        $this->assertArrayNotHasKey($subsection->cmid, $results['put']['cm']);
1659
 
1660
        // Validate activity indentation.
1661
        $mondinfo = get_fast_modinfo($course);
1662
        $this->assertEquals(1, $mondinfo->get_cm($otheractvity->cmid)->indent);
1663
        $this->assertEquals(1, $DB->get_field('course_modules', 'indent', ['id' => $otheractvity->cmid]));
1664
        $this->assertEquals(0, $mondinfo->get_cm($subsection->cmid)->indent);
1665
        $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $subsection->cmid]));
1666
 
1667
        // Now move left.
1668
        $updates = new stateupdates($courseformat);
1669
        $actions->cm_moveleft(
1670
            $updates,
1671
            $course,
1672
            [$subsection->cmid, $otheractvity->cmid],
1673
        );
1674
 
1675
        // Format results in a way we can compare easily.
1676
        $results = $this->summarize_updates($updates);
1677
 
1678
        // The state actions does not use create or remove actions because they are designed
1679
        // to refresh parts of the state.
1680
        $this->assertEquals(0, $results['create']['count']);
1681
        $this->assertEquals(0, $results['remove']['count']);
1682
 
1683
        // Mod subsection should be ignored.
1684
        $this->assertEquals(1, $results['put']['count']);
1685
 
1686
        // Validate course, section and cm.
1687
        $this->assertArrayHasKey($otheractvity->cmid, $results['put']['cm']);
1688
        $this->assertArrayNotHasKey($subsection->cmid, $results['put']['cm']);
1689
 
1690
        // Validate activity indentation.
1691
        $mondinfo = get_fast_modinfo($course);
1692
        $this->assertEquals(0, $mondinfo->get_cm($otheractvity->cmid)->indent);
1693
        $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $otheractvity->cmid]));
1694
        $this->assertEquals(0, $mondinfo->get_cm($subsection->cmid)->indent);
1695
        $this->assertEquals(0, $DB->get_field('course_modules', 'indent', ['id' => $subsection->cmid]));
1696
    }
1697
 
1698
    /**
1699
     * Test for filter_cms_with_section_delegate protected method.
1700
     *
1701
     * @covers ::filter_cms_with_section_delegate
1702
     */
1703
    public function test_filter_cms_with_section_delegate(): void {
1704
        $this->resetAfterTest();
1705
        $this->setAdminUser();
1706
 
1707
        $course = $this->getDataGenerator()->create_course();
1708
        $subsection = $this->getDataGenerator()->create_module('subsection', ['course' => $course]);
1709
        $otheractvity = $this->getDataGenerator()->create_module('forum', ['course' => $course]);
1710
        $this->setAdminUser();
1711
 
1712
        $courseformat = course_get_format($course->id);
1713
 
1714
        $modinfo = $courseformat->get_modinfo();
1715
        $subsectioninfo = $modinfo->get_cm($subsection->cmid);
1716
        $otheractvityinfo = $modinfo->get_cm($otheractvity->cmid);
1717
 
1718
        $actions = new stateactions();
1719
 
1720
        $method = new ReflectionMethod($actions, 'filter_cms_with_section_delegate');
1721
        $result = $method->invoke($actions, [$subsectioninfo, $otheractvityinfo]);
1722
 
1723
        $this->assertCount(1, $result);
1724
        $this->assertArrayHasKey($otheractvity->cmid, $result);
1725
        $this->assertArrayNotHasKey($subsection->cmid, $result);
1726
        $this->assertEquals($otheractvityinfo, $result[$otheractvityinfo->id]);
1727
    }
1728
 
1729
    /**
1730
     * Test for create_module public method.
1731
     *
1732
     * @covers ::create_module
1733
     */
1734
    public function test_create_module(): void {
1735
        $this->resetAfterTest();
1736
 
1737
        $modname = 'subsection';
1738
        $manager = \core_plugin_manager::resolve_plugininfo_class('mod');
1739
        $manager::enable_plugin($modname, 1);
1740
 
1741
        // Create a course with 1 section and 1 student.
1742
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1743
        $courseformat = course_get_format($course->id);
1744
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1745
 
1746
        $this->setAdminUser();
1747
 
1748
        // Sanity check.
1749
        $this->assertEmpty($courseformat->get_modinfo()->get_cms());
1750
 
1751
        // Execute given method.
1752
        $actions = new stateactions();
1753
        $updates = new stateupdates($courseformat);
1754
        $actions->create_module($updates, $course, $modname, $targetsection->sectionnum);
1755
 
1756
        // Validate cm was created and updates were generated.
1757
        $results = $this->summarize_updates($updates);
1758
        $cmupdate = reset($results['put']['cm']);
1759
        $this->assertCount(1, $courseformat->get_modinfo()->get_cms());
1760
        $this->assertEquals($modname, $cmupdate->module);
1761
        $this->assertEquals($targetsection->id, $cmupdate->sectionid);
1762
        $this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
1763
        $this->assertDebuggingCalled();
1764
    }
1765
 
1766
    /**
1767
     * Test for create_module public method with no capabilities.
1768
     *
1769
     * @covers ::create_module
1770
     */
1771
    public function test_create_module_no_capabilities(): void {
1772
        $this->resetAfterTest();
1773
 
1774
        $modname = 'subsection';
1775
 
1776
        // Create a course with 1 section and 1 student.
1777
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1778
        $student = $this->getDataGenerator()->create_user();
1779
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1780
        $courseformat = course_get_format($course->id);
1781
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1782
 
1783
        $this->setAdminUser();
1784
 
1785
        // Sanity check.
1786
        $this->assertEmpty($courseformat->get_modinfo()->get_cms());
1787
 
1788
        // Change to a user without permission.
1789
        $this->setUser($student);
1790
 
1791
        // Validate that the method throws an exception.
1792
        $actions = new stateactions();
1793
        $updates = new stateupdates($courseformat);
1794
 
1795
        // Capturing exceptions on deprecated methods is tricky because expectException is executed
1796
        // before assertDebuggingCalled. We need to use try catch in a creative way.
1797
        try {
1798
            $actions->create_module($updates, $course, $modname, $targetsection->sectionnum);
1799
        } catch (moodle_exception $exception) {
1800
            $this->assertDebuggingCalled();
1801
            $this->expectException(moodle_exception::class);
1802
            throw $exception;
1803
        }
1804
    }
1805
 
1806
    /**
1807
     * Test for create_module public method with targetcmid parameter.
1808
     *
1809
     * @covers ::create_module
1810
     */
1811
    public function test_create_module_with_targetcmid(): void {
1812
        $this->resetAfterTest();
1813
 
1814
        $modname = 'subsection';
1815
        $manager = \core_plugin_manager::resolve_plugininfo_class('mod');
1816
        $manager::enable_plugin($modname, 1);
1817
 
1818
        // Create a course with 1 section, 2 modules (forum and page) and 1 student.
1819
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1820
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course], ['section' => 1]);
1821
        $page = $this->getDataGenerator()->create_module('page', ['course' => $course], ['section' => 1]);
1822
        $student = $this->getDataGenerator()->create_user();
1823
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1824
        $courseformat = course_get_format($course->id);
1825
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1826
 
1827
        $this->setAdminUser();
1828
 
1829
        // Sanity check.
1830
        $this->assertCount(2, $courseformat->get_modinfo()->get_cms());
1831
 
1832
        // Execute given method.
1833
        $actions = new stateactions();
1834
        $updates = new stateupdates($courseformat);
1835
        $actions->create_module($updates, $course, $modname, $targetsection->sectionnum, $page->cmid);
1836
 
1837
        $modinfo = $courseformat->get_modinfo();
1838
        $cms = $modinfo->get_cms();
1839
        $results = $this->summarize_updates($updates);
1840
        $cmupdate = reset($results['put']['cm']);
1841
 
1842
        // Validate updates were generated.
1843
        $this->assertEquals($modname, $cmupdate->module);
1844
        $this->assertEquals($targetsection->id, $cmupdate->sectionid);
1845
        $this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
1846
 
1847
        // Validate that the new module was created between both modules.
1848
        $this->assertCount(3, $cms);
1849
        $this->assertArrayHasKey($cmupdate->id, $cms);
1850
        $this->assertEquals(
1851
            implode(',', [$forum->cmid, $cmupdate->id, $page->cmid]),
1852
            $modinfo->get_section_info(1)->sequence
1853
        );
1854
        $this->assertDebuggingCalled();
1855
    }
1856
 
1857
    /**
1858
     * Test for new_module public method.
1859
     *
1860
     * @covers ::new_module
1861
     */
1862
    public function test_new_module(): void {
1863
        $this->resetAfterTest();
1864
 
1865
        $modname = 'subsection';
1866
 
1867
        // Create a course with 1 section and 1 student.
1868
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1869
        $courseformat = course_get_format($course->id);
1870
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1871
 
1872
        $this->setAdminUser();
1873
 
1874
        // Sanity check.
1875
        $this->assertEmpty($courseformat->get_modinfo()->get_cms());
1876
 
1877
        // Execute given method.
1878
        $actions = new stateactions();
1879
        $updates = new stateupdates($courseformat);
1880
        $actions->new_module($updates, $course, $modname, $targetsection->id);
1881
 
1882
        // Validate cm was created and updates were generated.
1883
        $results = $this->summarize_updates($updates);
1884
        $cmupdate = reset($results['put']['cm']);
1885
        $this->assertCount(1, $courseformat->get_modinfo()->get_cms());
1886
        $this->assertEquals($modname, $cmupdate->module);
1887
        $this->assertEquals($targetsection->id, $cmupdate->sectionid);
1888
        $this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
1889
    }
1890
 
1891
    /**
1892
     * Test for new_module public method with no capabilities.
1893
     *
1894
     * @covers ::new_module
1895
     */
1896
    public function test_new_module_no_capabilities(): void {
1897
        $this->resetAfterTest();
1898
 
1899
        $modname = 'subsection';
1900
 
1901
        // Create a course with 1 section and 1 student.
1902
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1903
        $student = $this->getDataGenerator()->create_user();
1904
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1905
        $courseformat = course_get_format($course->id);
1906
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1907
 
1908
        $this->setAdminUser();
1909
 
1910
        // Sanity check.
1911
        $this->assertEmpty($courseformat->get_modinfo()->get_cms());
1912
 
1913
        // Change to a user without permission.
1914
        $this->setUser($student);
1915
 
1916
        // Validate that the method throws an exception.
1917
        $actions = new stateactions();
1918
        $updates = new stateupdates($courseformat);
1919
 
1920
        $this->expectException(moodle_exception::class);
1921
        $actions->new_module($updates, $course, $modname, $targetsection->id);
1922
    }
1923
 
1924
    /**
1925
     * Test for new_module public method with targetcmid parameter.
1926
     *
1927
     * @covers ::new_module
1928
     */
1929
    public function test_new_module_with_targetcmid(): void {
1930
        $this->resetAfterTest();
1931
 
1932
        $modname = 'subsection';
1933
 
1934
        // Create a course with 1 section, 2 modules (forum and page) and 1 student.
1935
        $course = $this->getDataGenerator()->create_course(['numsections' => 1]);
1936
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course], ['section' => 1]);
1937
        $page = $this->getDataGenerator()->create_module('page', ['course' => $course], ['section' => 1]);
1938
        $student = $this->getDataGenerator()->create_user();
1939
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
1940
        $courseformat = course_get_format($course->id);
1941
        $targetsection = $courseformat->get_modinfo()->get_section_info(1);
1942
 
1943
        $this->setAdminUser();
1944
 
1945
        // Sanity check.
1946
        $this->assertCount(2, $courseformat->get_modinfo()->get_cms());
1947
 
1948
        // Execute given method.
1949
        $actions = new stateactions();
1950
        $updates = new stateupdates($courseformat);
1951
        $actions->new_module($updates, $course, $modname, $targetsection->id, $page->cmid);
1952
 
1953
        $modinfo = $courseformat->get_modinfo();
1954
        $cms = $modinfo->get_cms();
1955
        $results = $this->summarize_updates($updates);
1956
        $cmupdate = reset($results['put']['cm']);
1957
 
1958
        // Validate updates were generated.
1959
        $this->assertEquals($modname, $cmupdate->module);
1960
        $this->assertEquals($targetsection->id, $cmupdate->sectionid);
1961
        $this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
1962
 
1963
        // Validate that the new module was created between both modules.
1964
        $this->assertCount(3, $cms);
1965
        $this->assertArrayHasKey($cmupdate->id, $cms);
1966
        $this->assertEquals(
1967
            implode(',', [$forum->cmid, $cmupdate->id, $page->cmid]),
1968
            $modinfo->get_section_info(1)->sequence
1969
        );
1970
    }
1 efrain 1971
}